shakha 0.2.0 → 0.8.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +153 -92
  4. data/SECURITY.md +56 -0
  5. data/app/controllers/shakha/application_controller.rb +3 -4
  6. data/app/controllers/shakha/auth_controller.rb +103 -196
  7. data/app/controllers/shakha/session_controller.rb +27 -59
  8. data/app/models/shakha/session.rb +27 -7
  9. data/app/models/shakha/user.rb +4 -8
  10. data/app/views/shakha/auth/new.html.erb +9 -21
  11. data/app/views/shakha/layouts/shakha.html.erb +1 -1
  12. data/lib/generators/shakha/install/install_generator.rb +84 -0
  13. data/lib/generators/shakha/install/templates/create_shakha_tables.rb.erb +27 -0
  14. data/lib/generators/shakha/install/templates/shakha.rb.erb +39 -0
  15. data/lib/shakha/config.rb +10 -30
  16. data/lib/shakha/config_validator.rb +1 -2
  17. data/lib/shakha/controller_helpers.rb +16 -33
  18. data/lib/shakha/engine.rb +10 -17
  19. data/lib/shakha/error_handler.rb +8 -4
  20. data/lib/shakha/pkce.rb +5 -4
  21. data/lib/shakha/providers/base.rb +27 -0
  22. data/lib/shakha/providers/github.rb +101 -0
  23. data/lib/shakha/providers/google.rb +113 -0
  24. data/lib/shakha/providers.rb +19 -0
  25. data/lib/shakha/rate_limiter.rb +5 -5
  26. data/lib/shakha/version.rb +2 -2
  27. data/lib/shakha.rb +4 -29
  28. metadata +34 -26
  29. data/app/controllers/shakha/jwks_controller.rb +0 -10
  30. data/app/controllers/shakha/openid_controller.rb +0 -21
  31. data/app/models/shakha/client.rb +0 -20
  32. data/app/views/shakha/auth/sessions.html.erb +0 -66
  33. data/lib/generators/shakha/install_generator.rb +0 -25
  34. data/lib/generators/shakha/templates/initializer.rb.erb +0 -29
  35. data/lib/generators/shakha/templates/migration.rb.erb +0 -45
  36. data/lib/shakha/auditable.rb +0 -47
  37. data/lib/shakha/jwt_handler.rb +0 -127
  38. data/lib/shakha/middleware.rb +0 -49
  39. data/lib/shakha/pairwise.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 366829b83286435b8a3ff8a9b74cdfe319e072768efabbbfa0777d3ce7a9bb6b
4
- data.tar.gz: 5aafc4d8d1db8bf6287c40ffea5dbb614903e288ddfeb35ae4d1c1e0b0ecff34
3
+ metadata.gz: 8f67f490e63b0d03cfa0d2abd6b31e68ced7a20e17093b6ccc77f15209b4a2cd
4
+ data.tar.gz: 75edc086b7684691be80bc23ed40950d839a1c26b4070edcbe14447153d61995
5
5
  SHA512:
6
- metadata.gz: 6a7982ee421c97e9f66eb5eceb29dddd2159806d37d37bb759e84c80686390f395703adfd8b881c7a0d5eafa3c5e2bc969844ad6375ce4a04572b5e4758b5f75
7
- data.tar.gz: f41c800d89c409fbcea81ab122d7f9e59028777b15643ec006ff0b6e0cddb7adcc621c463cdc538b088e171f9f4eb460cf5197b48b361700a041373c5899b2f7
6
+ metadata.gz: 3797ed65b6d60130ac0afbe7dcb11790cacb4ab46feff2d74125575377a8b633aa290b5db2edf1d61b8fa87f3c66ce3980389424cca5d4d8394b729d67012a20
7
+ data.tar.gz: 9cc96c35365fdee1025a878d7abf344f0755e37cd14f94995566723a27f7a978e86532f4065a34ca9eec0b472c1cfd879509ee4f22c6c9faf6948fc5f17e326c
data/CHANGELOG.md ADDED
@@ -0,0 +1,104 @@
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.8.0] — 2026-07-27
11
+
12
+ First public release of the reworked gem.
13
+
14
+ ### Added
15
+ - Rewritten README, `CHANGELOG.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
16
+ issue/PR templates, and Dependabot.
17
+
18
+ ### Changed
19
+ - Gemspec: accurate summary/description, GitHub homepage, `source_code_uri` /
20
+ `changelog_uri` / `bug_tracker_uri`, `rubygems_mfa_required`, and an explicit
21
+ packaged-file manifest.
22
+ - Removed superseded planning directories from the repo (kept in git history).
23
+
24
+ ## [0.7.0] — 2026-07-26
25
+
26
+ Security hardening. Several changes are breaking; there are no production
27
+ installs of the intermediate 0.6.0 to migrate.
28
+
29
+ ### Security
30
+ - Verify the Google ID token: the OIDC `nonce` is now threaded through the
31
+ authorize step and checked on callback, and the `iss`, `aud`, and `exp` claims
32
+ are validated. The `aud` check rejects tokens minted for other clients.
33
+ - Deliver the session token via a single-use, 60-second exchange code by default
34
+ instead of placing it in the redirect URL, where it could leak into history,
35
+ `Referer` headers, and logs.
36
+ - Validate `return_to` against the app origin and `allowed_redirect_origins`
37
+ before it is stored, closing an open-redirect path in the OAuth flow.
38
+ - Run `bundler-audit` in CI; raise the `jwt` floor to `>= 2.10.3` and patch
39
+ `rails-html-sanitizer`, `json`, and `concurrent-ruby`.
40
+ - Add `SECURITY.md` with a private disclosure process and threat model.
41
+
42
+ ### Changed
43
+ - **Breaking:** the OAuth callback now redirects with `?code=` (a one-time
44
+ exchange code). Set `config.redirect_token_delivery = :token` to restore the
45
+ previous `?token=` behavior for one release.
46
+ - **Breaking:** removed the `shakha_clients` table and the `Shakha::Client`
47
+ model. Users and sessions no longer belong to a client. The sign-in page shows
48
+ `config.app_name` (default `"Shakha"`).
49
+ - Returning users' `email`/`name`/`picture` are refreshed on each sign-in.
50
+ - The OAuth `redirect_uri` is derived from the engine's actual mount point, so
51
+ Shakha works when mounted anywhere.
52
+ - An unknown or disabled provider returns `404` instead of raising a `500`.
53
+
54
+ ### Added
55
+ - `config.redirect_token_delivery`, `config.app_name`.
56
+ - `POST /auth/shakha/session/exchange` — swap a one-time code for the token.
57
+ - The install generator injects `ActionDispatch::Cookies` into API-only hosts.
58
+
59
+ ## [0.6.0] — 2026-07-26
60
+
61
+ ### Added
62
+ - A real test suite: an in-process dummy Rails app harness with full coverage of
63
+ the OAuth flow, session/bearer auth, providers, PKCE, models, and the
64
+ generator. Previously the suite could not load.
65
+ - GitHub Actions CI across Ruby 3.1–3.4 × Rails 7.1/8.0/8.1, plus RuboCop
66
+ (`rubocop-rails-omakase`).
67
+
68
+ ### Fixed
69
+ - PKCE code verifier no longer carries base64 padding (`SecureRandom.urlsafe_base64`
70
+ takes `padding` positionally, so `padding: false` had passed a truthy hash).
71
+ - The OAuth callback no longer trips Rails' open-redirect protection.
72
+ - The install generator injects into `ApplicationController` using the
73
+ generator's `destination_root` rather than a CWD-relative path.
74
+
75
+ ### Changed
76
+ - **Breaking (API-mode hosts):** the engine no longer force-inserts cookie and
77
+ session middleware into the host app. Re-run `rails generate shakha:install`,
78
+ or add `ActionDispatch::Cookies` to your middleware stack.
79
+
80
+ ## [0.5.0] — 2026-05-20
81
+ - End-to-end verification of the SPA flow in a Rails 8.1 API app; runtime fixes
82
+ found during that testing; API-mode cookie/session compatibility.
83
+
84
+ ## [0.3.0] — 2026-05-19
85
+ - Multi-provider system (Google and GitHub) wired into the engine; simplified
86
+ provider-scoped URLs; `rails generate shakha:install` generator.
87
+
88
+ ## [0.2.0] — 2026-05-19
89
+ - Rework toward an SPA-first broker: fixed six fatal bugs and several security
90
+ issues; rewrote the auth flow and controllers; simplified models and config;
91
+ removed the JWT/JWKS/OIDC and pairwise-subject service infrastructure.
92
+
93
+ ## [0.1.0] — 2026-05
94
+ - Initial prototype: Google OAuth broker with PKCE, database sessions, and a
95
+ sign-in page.
96
+
97
+ [Unreleased]: https://github.com/Asrat77/shakha/compare/v0.8.0...HEAD
98
+ [0.8.0]: https://github.com/Asrat77/shakha/compare/v0.7.0...v0.8.0
99
+ [0.7.0]: https://github.com/Asrat77/shakha/compare/v0.6.0...v0.7.0
100
+ [0.6.0]: https://github.com/Asrat77/shakha/compare/v0.5.0...v0.6.0
101
+ [0.5.0]: https://github.com/Asrat77/shakha/compare/v0.3.0...v0.5.0
102
+ [0.3.0]: https://github.com/Asrat77/shakha/compare/v0.2.0...v0.3.0
103
+ [0.2.0]: https://github.com/Asrat77/shakha/compare/v0.1.0...v0.2.0
104
+ [0.1.0]: https://github.com/Asrat77/shakha/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,94 +1,117 @@
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 :pairwise_sub, null: false
32
- t.string :email
33
- t.string :name
34
- t.string :picture
35
- t.timestamps
36
- t.index :pairwise_sub, unique: true
37
- t.index :email
38
- end
39
-
40
- create_table :shakha_sessions do |t|
41
- t.references :user, foreign_key: { to_table: :shakha_users }
42
- t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
43
- t.string :token, null: false
44
- t.string :jti, null: false
45
- t.string :ip_address
46
- t.string :user_agent
47
- t.timestamps
48
- t.index :token, unique: true
49
- t.index :jti, unique: true
50
- t.index :created_at
51
- end
52
- end
53
- end
38
+ gem "shakha"
54
39
  ```
55
40
 
56
- ## Configuration
57
-
58
- Create `config/initializers/shakha.rb`:
41
+ Mount the engine in `config/routes.rb`:
59
42
 
60
43
  ```ruby
61
- Shakha.setup do |config|
62
- config.app_origin = ENV.fetch("SHAKHA_APP_ORIGIN", "http://localhost:3000")
63
- config.service_url = ENV["SHAKHA_SERVICE_URL"] # omit for embedded mode
64
- config.service_secret = ENV["SHAKHA_SERVICE_SECRET"]
65
- config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
66
- config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
67
- config.session_lifetime = 30.days
68
- end
44
+ mount Shakha::Engine => "/auth/shakha"
69
45
  ```
70
46
 
71
- Environment variables:
47
+ Run the installer and migrate:
72
48
 
73
49
  ```bash
74
- export SHAKHA_APP_ORIGIN="https://yourapp.com"
75
- export SHAKHA_SERVICE_SECRET="your-secret-key"
76
- export GOOGLE_CLIENT_ID="your-google-client-id"
77
- export GOOGLE_CLIENT_SECRET="your-google-client-secret"
50
+ bin/rails generate shakha:install
51
+ bin/rails db:migrate
78
52
  ```
79
53
 
80
- Google Cloud Console redirect URI: `https://yourapp.com/auth/shakha/callback`
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.
57
+
58
+ ### Configuration
59
+
60
+ Set these environment variables (the generated initializer reads them):
61
+
62
+ | Variable | Required | Purpose |
63
+ |---|---|---|
64
+ | `APP_ORIGIN` | yes | Your Rails app's origin, e.g. `https://api.example.com` |
65
+ | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | for Google | Google OAuth credentials |
66
+ | `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | for GitHub | GitHub OAuth credentials |
67
+ | `ALLOWED_REDIRECT_ORIGINS` | for a cross-origin SPA | Comma-separated frontend origins allowed as `return_to`, e.g. `https://app.example.com` |
68
+
69
+ Provider console redirect URIs (derived from `APP_ORIGIN` and the mount point):
70
+
71
+ - Google: `https://api.example.com/auth/shakha/google/callback`
72
+ - GitHub: `https://api.example.com/auth/shakha/github/callback`
73
+
74
+ ## Frontend (React) example
75
+
76
+ ```jsx
77
+ // Start sign-in
78
+ const signIn = () => {
79
+ const returnTo = "https://app.example.com/login";
80
+ window.location =
81
+ `https://api.example.com/auth/shakha/google?return_to=${encodeURIComponent(returnTo)}`;
82
+ };
83
+
84
+ // On the return page (/login), swap the one-time code for a token
85
+ useEffect(() => {
86
+ const code = new URLSearchParams(window.location.search).get("code");
87
+ if (!code) return;
88
+
89
+ fetch("https://api.example.com/auth/shakha/session/exchange", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ code }),
93
+ })
94
+ .then((r) => r.json())
95
+ .then(({ token }) => {
96
+ localStorage.setItem("token", token);
97
+ return fetch("https://api.example.com/auth/shakha/session", {
98
+ headers: { Authorization: `Bearer ${token}` },
99
+ });
100
+ })
101
+ .then((r) => r.json())
102
+ .then(({ user }) => setUser(user));
103
+ }, []);
104
+ ```
81
105
 
82
- ## Usage
106
+ ## Rails monolith usage
83
107
 
84
- ### Sign In
108
+ Shakha ships a minimal sign-in page and sets an encrypted session cookie, so a
109
+ classic server-rendered app needs no JavaScript.
85
110
 
86
111
  ```erb
87
- <%= link_to "Sign in with Google", shakha.new_auth_path %>
112
+ <%= link_to "Sign in", "/auth/shakha/google" %>
88
113
  ```
89
114
 
90
- ### Protect Routes
91
-
92
115
  ```ruby
93
116
  class ApplicationController < ActionController::Base
94
117
  include Shakha::ControllerHelpers
@@ -96,48 +119,86 @@ class ApplicationController < ActionController::Base
96
119
  end
97
120
  ```
98
121
 
99
- ### Current User
100
-
101
122
  ```ruby
102
- current_user # Shakha::User or nil
103
- current_session # Shakha::Session or nil
104
- signed_in? # boolean
105
- authenticate! # redirects to login if not signed in
123
+ current_user # Shakha::User or nil
124
+ current_session # Shakha::Session or nil
125
+ signed_in? # boolean
126
+ authenticate! # 401 JSON for API requests, redirect to sign-in for HTML
106
127
  ```
107
128
 
108
- ### Sign Out
129
+ Sign out:
109
130
 
110
131
  ```erb
111
- <%= link_to "Sign out", shakha.session_path, data: { turbo_method: :delete } %>
132
+ <%= button_to "Sign out", "/auth/shakha/sign_out", method: :delete %>
112
133
  ```
113
134
 
114
- ### JWT Verification (API Mode)
135
+ ## Endpoints
115
136
 
116
- ```ruby
117
- payload = Shakha.verify_token(id_token)
118
- user_id = payload[:pairwise_sub]
119
- ```
137
+ All paths are relative to the mount point (`/auth/shakha` above).
138
+
139
+ | Method | Path | Auth | Purpose |
140
+ |---|---|---|---|
141
+ | GET | `/` | — | Built-in sign-in page (monolith) |
142
+ | GET | `/:provider` | — | Start OAuth (`google`, `github`) |
143
+ | GET | `/:provider/callback` | — | OAuth callback (provider redirects here) |
144
+ | POST | `/session/exchange` | one-time code | Swap the code for `{ token, expires_at }` |
145
+ | GET | `/session` | cookie or bearer | Current user: `{ user: {...}, session: {...} }` |
146
+ | GET | `/session/check` | cookie or bearer | Lightweight `{ status: "active" }` / `401 { status: "expired" }` |
147
+ | DELETE | `/sign_out` | cookie or bearer | Destroy the session |
148
+
149
+ ## Configuration reference
120
150
 
121
- ## Architecture
151
+ Set in `config/initializers/shakha.rb` via `Shakha.setup { |config| ... }`.
122
152
 
123
- - **PKCE** S256 code challenges on every flow
124
- - **Pairwise subjects** — domain-scoped user identifiers (HMAC-SHA256)
125
- - **ES256 JWTs**signed with JWKS endpoint at `.well-known/jwks.json`
126
- - **OpenID Connect** `.well-known/openid-configuration` endpoint
127
- - **Database sessions**DHH-style, no Redis
128
- - **Turbo native**zero JavaScript needed
129
- - **Embedded or standalone** runs as Rails engine or headless service
153
+ | Option | Default | Description |
154
+ |---|---|---|
155
+ | `app_origin` || Your Rails app's origin; used to build the OAuth `redirect_uri` |
156
+ | `app_name` | `"Shakha"` | Name shown on the built-in sign-in page |
157
+ | `google_client_id` / `google_client_secret` | | Google OAuth credentials |
158
+ | `github_client_id` / `github_client_secret` | | GitHub OAuth credentials |
159
+ | `providers` | `[:google]` | Enabled providers |
160
+ | `allowed_redirect_origins` | `nil` | Extra origins permitted as `return_to` targets |
161
+ | `redirect_token_delivery` | `:exchange_code` | `:exchange_code` (one-time code) or `:token` (token in the URL) |
162
+ | `session_lifetime` | `30.days` | How long a session stays valid |
163
+ | `rate_limiting_enabled` | `false` | Rate-limit `authorize` (20/min) and `callback` (10/min) |
130
164
 
131
- ## Modes
165
+ ## Security
132
166
 
133
- ### Embedded (default)
167
+ Shakha uses PKCE (S256), a timing-safe `state` check, and Google `nonce` +
168
+ ID-token claim (`iss`/`aud`/`exp`) verification. Session tokens are random
169
+ strings in the database — deleting the row revokes access immediately. See
170
+ [SECURITY.md](SECURITY.md) for the full threat model and how to report a
171
+ vulnerability.
134
172
 
135
- 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.
173
+ ## How it compares
136
174
 
137
- ### Service (multi-tenant)
175
+ - **OmniAuth** — a Rack middleware for the OAuth handshake; you still build
176
+ sessions, endpoints, and the SPA token hand-off yourself. Shakha ships the
177
+ whole path.
178
+ - **Devise** — excellent for password auth in a monolith; OAuth and API/SPA
179
+ token flows are add-ons. Shakha is OAuth-only and API-first.
180
+ - **Rodauth** — broader and more configurable, with its own conventions. Shakha
181
+ does one thing: an OAuth session broker for the API + SPA era.
182
+
183
+ ## Writing your own provider
184
+
185
+ A provider implements five methods (`provider_name`, `scopes`, `authorize_url`,
186
+ `exchange_code`, `identity_from_response`) returning an identity hash of
187
+ `{ provider:, uid:, email:, name:, picture: }`. See
188
+ [`Shakha::Providers::Base`](lib/shakha/providers/base.rb) and the Google/GitHub
189
+ implementations alongside it.
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ bundle install
195
+ bundle exec rake test
196
+ bundle exec rubocop
197
+ ```
138
198
 
139
- Set `SHAKHA_SERVICE_URL` and register each app's origin in `shakha_clients`. Each app gets different pairwise subjects for the same Google user.
199
+ Test against another Rails version with `BUNDLE_GEMFILE=gemfiles/rails_71.gemfile`.
200
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
140
201
 
141
202
  ## License
142
203
 
143
- MIT
204
+ 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
 
@@ -14,8 +13,8 @@ module Shakha
14
13
 
15
14
  private
16
15
 
17
- def invalid_csrf_token(exception)
16
+ def invalid_csrf_token(_exception)
18
17
  render json: { error: "Invalid CSRF token" }, status: :unprocessable_entity
19
18
  end
20
19
  end
21
- end
20
+ end