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 +4 -4
- data/CHANGELOG.md +128 -0
- data/README.md +174 -96
- data/SECURITY.md +56 -0
- data/app/controllers/shakha/application_controller.rb +2 -3
- data/app/controllers/shakha/auth_controller.rb +52 -32
- data/app/controllers/shakha/session_controller.rb +13 -0
- data/app/controllers/shakha/sessions_controller.rb +33 -0
- data/app/models/shakha/session.rb +31 -2
- data/app/models/shakha/user.rb +1 -3
- data/app/views/shakha/auth/new.html.erb +4 -4
- data/app/views/shakha/layouts/shakha.html.erb +1 -1
- data/lib/generators/shakha/install/install_generator.rb +103 -0
- data/lib/generators/shakha/install/templates/create_shakha_tables.rb.erb +27 -0
- data/lib/generators/shakha/install/templates/shakha.rb.erb +42 -0
- data/lib/shakha/config.rb +6 -2
- data/lib/shakha/config_validator.rb +11 -2
- data/lib/shakha/controller_helpers.rb +6 -4
- data/lib/shakha/engine.rb +12 -7
- data/lib/shakha/error_handler.rb +6 -1
- data/lib/shakha/pkce.rb +5 -4
- data/lib/shakha/providers/base.rb +2 -2
- data/lib/shakha/providers/github.rb +4 -2
- data/lib/shakha/providers/google.rb +26 -3
- data/lib/shakha/providers.rb +23 -3
- data/lib/shakha/rate_limiter.rb +5 -5
- data/lib/shakha/version.rb +2 -2
- data/lib/shakha.rb +2 -1
- metadata +31 -19
- data/app/models/shakha/client.rb +0 -16
- data/lib/generators/shakha/install_generator.rb +0 -25
- data/lib/generators/shakha/templates/initializer.rb.erb +0 -29
- data/lib/generators/shakha/templates/migration.rb.erb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d366a8476de38c1c5ba4a9a1b5cc286b96ef0f99c20e08101c94a4048621a63c
|
|
4
|
+
data.tar.gz: 7a79e8f4493e93a4b1d6e2d9711ad0bbbd9eede57fede0de68ce667d184d82a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
3
|
+
**SPA-first OAuth for Rails. One redirect. One token. Done.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/Asrat77/shakha/actions/workflows/ci.yml)
|
|
6
|
+
[](https://rubygems.org/gems/shakha)
|
|
7
|
+
[](LICENSE.txt)
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
gem "shakha"
|
|
11
|
-
```
|
|
15
|
+
No JWTs to issue or rotate. No Redis. No frontend SDK.
|
|
12
16
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
Create `config/initializers/shakha.rb`:
|
|
41
|
+
Mount the engine in `config/routes.rb`:
|
|
58
42
|
|
|
59
43
|
```ruby
|
|
60
|
-
Shakha
|
|
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
|
-
|
|
47
|
+
Run the installer and migrate:
|
|
71
48
|
|
|
72
49
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
109
|
+
For a complete runnable version (Rails API + Vite/React), see
|
|
110
|
+
[examples/rails-api-react](examples/rails-api-react).
|
|
80
111
|
|
|
81
|
-
##
|
|
112
|
+
## Rails monolith usage
|
|
82
113
|
|
|
83
|
-
|
|
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
|
|
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
|
|
102
|
-
current_session
|
|
103
|
-
signed_in?
|
|
104
|
-
authenticate!
|
|
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
|
-
|
|
135
|
+
Sign out:
|
|
108
136
|
|
|
109
137
|
```erb
|
|
110
|
-
<%=
|
|
138
|
+
<%= button_to "Sign out", "/auth/shakha/sign_out", method: :delete %>
|
|
111
139
|
```
|
|
112
140
|
|
|
113
|
-
|
|
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
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
209
|
+
```bash
|
|
210
|
+
bundle install
|
|
211
|
+
bundle exec rake test
|
|
212
|
+
bundle exec rubocop
|
|
118
213
|
```
|
|
119
214
|
|
|
120
|
-
|
|
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
|