shakha 0.8.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 +25 -1
- data/README.md +20 -4
- data/app/controllers/shakha/auth_controller.rb +12 -1
- data/app/controllers/shakha/sessions_controller.rb +33 -0
- data/app/models/shakha/session.rb +4 -0
- data/lib/generators/shakha/install/install_generator.rb +33 -14
- data/lib/generators/shakha/install/templates/shakha.rb.erb +7 -4
- data/lib/shakha/config_validator.rb +11 -2
- data/lib/shakha/engine.rb +2 -0
- data/lib/shakha/providers.rb +23 -3
- data/lib/shakha/version.rb +1 -1
- metadata +2 -1
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
CHANGED
|
@@ -7,6 +7,29 @@ versions may include breaking changes; these are called out below.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
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
|
+
|
|
10
33
|
## [0.8.0] — 2026-07-27
|
|
11
34
|
|
|
12
35
|
First public release of the reworked gem.
|
|
@@ -94,7 +117,8 @@ installs of the intermediate 0.6.0 to migrate.
|
|
|
94
117
|
- Initial prototype: Google OAuth broker with PKCE, database sessions, and a
|
|
95
118
|
sign-in page.
|
|
96
119
|
|
|
97
|
-
[Unreleased]: https://github.com/Asrat77/shakha/compare/v0.
|
|
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
|
|
98
122
|
[0.8.0]: https://github.com/Asrat77/shakha/compare/v0.7.0...v0.8.0
|
|
99
123
|
[0.7.0]: https://github.com/Asrat77/shakha/compare/v0.6.0...v0.7.0
|
|
100
124
|
[0.6.0]: https://github.com/Asrat77/shakha/compare/v0.5.0...v0.6.0
|
data/README.md
CHANGED
|
@@ -53,7 +53,10 @@ bin/rails db:migrate
|
|
|
53
53
|
|
|
54
54
|
The generator writes a migration and `config/initializers/shakha.rb`, includes
|
|
55
55
|
`Shakha::ControllerHelpers` in your `ApplicationController`, and — for API-only
|
|
56
|
-
apps — adds the cookie middleware Shakha needs.
|
|
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.
|
|
57
60
|
|
|
58
61
|
### Configuration
|
|
59
62
|
|
|
@@ -103,6 +106,9 @@ useEffect(() => {
|
|
|
103
106
|
}, []);
|
|
104
107
|
```
|
|
105
108
|
|
|
109
|
+
For a complete runnable version (Rails API + Vite/React), see
|
|
110
|
+
[examples/rails-api-react](examples/rails-api-react).
|
|
111
|
+
|
|
106
112
|
## Rails monolith usage
|
|
107
113
|
|
|
108
114
|
Shakha ships a minimal sign-in page and sets an encrypted session cookie, so a
|
|
@@ -145,6 +151,8 @@ All paths are relative to the mount point (`/auth/shakha` above).
|
|
|
145
151
|
| GET | `/session` | cookie or bearer | Current user: `{ user: {...}, session: {...} }` |
|
|
146
152
|
| GET | `/session/check` | cookie or bearer | Lightweight `{ status: "active" }` / `401 { status: "expired" }` |
|
|
147
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) |
|
|
148
156
|
|
|
149
157
|
## Configuration reference
|
|
150
158
|
|
|
@@ -170,6 +178,13 @@ strings in the database — deleting the row revokes access immediately. See
|
|
|
170
178
|
[SECURITY.md](SECURITY.md) for the full threat model and how to report a
|
|
171
179
|
vulnerability.
|
|
172
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
|
+
|
|
173
188
|
## How it compares
|
|
174
189
|
|
|
175
190
|
- **OmniAuth** — a Rack middleware for the OAuth handshake; you still build
|
|
@@ -184,9 +199,10 @@ vulnerability.
|
|
|
184
199
|
|
|
185
200
|
A provider implements five methods (`provider_name`, `scopes`, `authorize_url`,
|
|
186
201
|
`exchange_code`, `identity_from_response`) returning an identity hash of
|
|
187
|
-
`{ provider:, uid:, email:, name:, picture: }
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
190
206
|
|
|
191
207
|
## Development
|
|
192
208
|
|
|
@@ -43,6 +43,10 @@ module Shakha
|
|
|
43
43
|
user = find_or_create_user(provider.provider_name, identity)
|
|
44
44
|
session_record = create_session(user)
|
|
45
45
|
set_session_cookie(session_record)
|
|
46
|
+
ActiveSupport::Notifications.instrument(
|
|
47
|
+
"shakha.sign_in",
|
|
48
|
+
user_id: user.id, provider: provider.provider_name, ip: request.remote_ip
|
|
49
|
+
)
|
|
46
50
|
redirect_to build_return_url(pkce_result[:return_to], session_record), allow_other_host: true
|
|
47
51
|
|
|
48
52
|
rescue PKCEError, OAuthError => e
|
|
@@ -50,7 +54,10 @@ module Shakha
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def destroy
|
|
53
|
-
current_session
|
|
57
|
+
if current_session
|
|
58
|
+
ActiveSupport::Notifications.instrument("shakha.sign_out", user_id: current_session.user_id)
|
|
59
|
+
current_session.destroy
|
|
60
|
+
end
|
|
54
61
|
cookies.delete(:shakha_session_token)
|
|
55
62
|
|
|
56
63
|
if request.format.json?
|
|
@@ -127,6 +134,10 @@ module Shakha
|
|
|
127
134
|
end
|
|
128
135
|
|
|
129
136
|
def handle_auth_failure(exception, pkce_result)
|
|
137
|
+
ActiveSupport::Notifications.instrument(
|
|
138
|
+
"shakha.auth_failure",
|
|
139
|
+
provider: params[:provider]&.to_sym, error: exception.class.name
|
|
140
|
+
)
|
|
130
141
|
return_to = pkce_result&.dig(:return_to) || "/"
|
|
131
142
|
|
|
132
143
|
if request.format.json? || api_request?
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shakha
|
|
4
|
+
class SessionsController < ApplicationController
|
|
5
|
+
before_action :authenticate!
|
|
6
|
+
|
|
7
|
+
# GET /sessions — the current user's active sessions, newest first.
|
|
8
|
+
def index
|
|
9
|
+
sessions = Shakha::Session.for(current_user).map do |session_record|
|
|
10
|
+
{
|
|
11
|
+
id: session_record.id,
|
|
12
|
+
ip_address: session_record.ip_address,
|
|
13
|
+
user_agent: session_record.user_agent,
|
|
14
|
+
created_at: session_record.created_at.iso8601,
|
|
15
|
+
current: session_record.id == current_session.id
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
render json: { sessions: sessions }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# DELETE /sessions/:id — revoke one of the current user's sessions.
|
|
23
|
+
def destroy
|
|
24
|
+
session_record = Shakha::Session.for(current_user).find_by(id: params[:id])
|
|
25
|
+
return render json: { error: "Session not found" }, status: :not_found unless session_record
|
|
26
|
+
|
|
27
|
+
session_record.destroy
|
|
28
|
+
cookies.delete(:shakha_session_token) if session_record.id == current_session.id
|
|
29
|
+
|
|
30
|
+
render json: { status: "revoked" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -8,14 +8,31 @@ module Shakha
|
|
|
8
8
|
include ActiveRecord::Generators::Migration
|
|
9
9
|
source_root File.expand_path("templates", __dir__)
|
|
10
10
|
|
|
11
|
+
BUILT_IN_PROVIDERS = %w[google github].freeze
|
|
12
|
+
|
|
11
13
|
desc "Installs Shakha — headless OAuth broker for Rails"
|
|
12
14
|
|
|
15
|
+
class_option :providers, type: :string, default: "google,github",
|
|
16
|
+
desc: "Comma-separated providers to configure (#{BUILT_IN_PROVIDERS.join(', ')})"
|
|
17
|
+
|
|
18
|
+
def validate_providers
|
|
19
|
+
unknown = providers - BUILT_IN_PROVIDERS
|
|
20
|
+
raise Thor::Error, "Unknown provider(s): #{unknown.join(', ')}. Choose from: #{BUILT_IN_PROVIDERS.join(', ')}" if unknown.any?
|
|
21
|
+
raise Thor::Error, "--providers must name at least one provider" if providers.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
13
24
|
def copy_migration
|
|
14
25
|
migration_template "create_shakha_tables.rb.erb", "db/migrate/create_shakha_tables.rb"
|
|
15
26
|
end
|
|
16
27
|
|
|
17
28
|
def copy_initializer
|
|
18
|
-
|
|
29
|
+
path = "config/initializers/shakha.rb"
|
|
30
|
+
if File.exist?(File.join(destination_root, path)) && !options[:force]
|
|
31
|
+
say_status :exist, path, :blue
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
template "shakha.rb.erb", path
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
def inject_application_controller
|
|
@@ -30,6 +47,7 @@ module Shakha
|
|
|
30
47
|
|
|
31
48
|
def enable_cookies_for_api_mode
|
|
32
49
|
return unless api_only_app?
|
|
50
|
+
return if File.read(File.join(destination_root, "config/application.rb")).include?("ActionDispatch::Cookies")
|
|
33
51
|
|
|
34
52
|
application "config.middleware.use ActionDispatch::Cookies"
|
|
35
53
|
say_status :insert, "config/application.rb -> ActionDispatch::Cookies (API mode)", :green
|
|
@@ -43,26 +61,23 @@ module Shakha
|
|
|
43
61
|
say " #{'─' * 50}", :green
|
|
44
62
|
say ""
|
|
45
63
|
say " 1. Set environment variables:", :yellow
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
providers.each do |provider|
|
|
65
|
+
say " #{provider.upcase}_CLIENT_ID / #{provider.upcase}_CLIENT_SECRET", :cyan
|
|
66
|
+
end
|
|
48
67
|
say ""
|
|
49
|
-
say " 2.
|
|
50
|
-
say " GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET", :cyan
|
|
68
|
+
say " 2. For SPA: set ALLOWED_REDIRECT_ORIGINS", :yellow
|
|
51
69
|
say ""
|
|
52
|
-
say " 3.
|
|
53
|
-
say ""
|
|
54
|
-
say " 4. Run migrations:", :yellow
|
|
70
|
+
say " 3. Run migrations:", :yellow
|
|
55
71
|
say " bin/rails db:migrate", :cyan
|
|
56
72
|
say ""
|
|
57
|
-
say "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
say " #{origin}/auth/shakha/github/callback", :cyan
|
|
73
|
+
say " 4. OAuth app redirect / callback URLs:", :yellow
|
|
74
|
+
providers.each do |provider|
|
|
75
|
+
say " #{origin}/auth/shakha/#{provider}/callback", :cyan
|
|
76
|
+
end
|
|
62
77
|
say " #{'─' * 50}", :green
|
|
63
78
|
say ""
|
|
64
79
|
say " Tell your frontend dev:", :cyan
|
|
65
|
-
say " Sign in: #{origin}/auth/shakha
|
|
80
|
+
say " Sign in: #{origin}/auth/shakha/#{providers.first}"
|
|
66
81
|
say " Session: GET #{origin}/auth/shakha/session"
|
|
67
82
|
say " Auth: Authorization: Bearer <token>"
|
|
68
83
|
say " Sign out: DELETE #{origin}/auth/shakha/sign_out"
|
|
@@ -71,6 +86,10 @@ module Shakha
|
|
|
71
86
|
|
|
72
87
|
private
|
|
73
88
|
|
|
89
|
+
def providers
|
|
90
|
+
@providers ||= options[:providers].to_s.split(",").map { |p| p.strip.downcase }.reject(&:empty?).uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
74
93
|
def migration_version
|
|
75
94
|
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
76
95
|
end
|
|
@@ -19,17 +19,20 @@ Shakha.setup do |config|
|
|
|
19
19
|
# (simpler, but the token is exposed in history/logs/Referer).
|
|
20
20
|
# config.redirect_token_delivery = :token
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
<% if providers.include?("google") -%>
|
|
23
|
+
# Google OAuth
|
|
23
24
|
config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
24
25
|
config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
<% end -%>
|
|
28
|
+
<% if providers.include?("github") -%>
|
|
29
|
+
# GitHub OAuth
|
|
27
30
|
config.github_client_id = ENV["GITHUB_CLIENT_ID"]
|
|
28
31
|
config.github_client_secret = ENV["GITHUB_CLIENT_SECRET"]
|
|
29
32
|
|
|
33
|
+
<% end -%>
|
|
30
34
|
# Enabled providers
|
|
31
|
-
|
|
32
|
-
config.providers = [:google, :github] # Both
|
|
35
|
+
config.providers = [ <%= providers.map { |p| ":#{p}" }.join(", ") %> ]
|
|
33
36
|
|
|
34
37
|
# Session lifetime (default: 30 days)
|
|
35
38
|
# config.session_lifetime = 30.days
|
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
module Shakha
|
|
4
4
|
module ConfigValidator
|
|
5
|
+
BUILT_IN_CREDENTIALS = {
|
|
6
|
+
google: %i[google_client_id google_client_secret],
|
|
7
|
+
github: %i[github_client_id github_client_secret]
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
5
10
|
class << self
|
|
6
11
|
def validate!(config)
|
|
7
12
|
missing = []
|
|
8
13
|
missing << "APP_ORIGIN" unless config.app_origin.present?
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
|
|
15
|
+
Array(config.providers).each do |provider|
|
|
16
|
+
BUILT_IN_CREDENTIALS.fetch(provider.to_sym, []).each do |attr|
|
|
17
|
+
missing << attr.to_s.upcase unless config.public_send(attr).present?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
11
20
|
|
|
12
21
|
unless missing.empty?
|
|
13
22
|
message = "Shakha: missing required configuration: #{missing.join(', ')}"
|
data/lib/shakha/engine.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Shakha
|
|
|
15
15
|
get "session" => "session#show"
|
|
16
16
|
get "session/check" => "session#check"
|
|
17
17
|
post "session/exchange" => "session#exchange"
|
|
18
|
+
get "sessions" => "sessions#index", defaults: { format: :json }
|
|
19
|
+
delete "sessions/:id" => "sessions#destroy", defaults: { format: :json }, as: :revoke_session
|
|
18
20
|
delete "sign_out" => "auth#destroy"
|
|
19
21
|
get "error" => "auth#error"
|
|
20
22
|
|
data/lib/shakha/providers.rb
CHANGED
|
@@ -11,9 +11,29 @@ module Shakha
|
|
|
11
11
|
github: "Shakha::Providers::GitHub"
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
@registry = PROVIDER_MAP.dup
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Registers a provider under +name+. +klass+ is a Providers::Base
|
|
18
|
+
# subclass or its name as a String (a String survives code reloading in
|
|
19
|
+
# development). Registering an existing name replaces it.
|
|
20
|
+
def register(name, klass)
|
|
21
|
+
@registry[name.to_sym] = klass
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def registered
|
|
25
|
+
@registry.keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve(name)
|
|
29
|
+
klass = @registry[name.to_sym] || raise(ConfigurationError, "Unknown provider: #{name}")
|
|
30
|
+
klass = klass.constantize if klass.is_a?(String)
|
|
31
|
+
klass.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset!
|
|
35
|
+
@registry = PROVIDER_MAP.dup
|
|
36
|
+
end
|
|
17
37
|
end
|
|
18
38
|
end
|
|
19
39
|
end
|
data/lib/shakha/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shakha
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
@@ -90,6 +90,7 @@ files:
|
|
|
90
90
|
- app/controllers/shakha/application_controller.rb
|
|
91
91
|
- app/controllers/shakha/auth_controller.rb
|
|
92
92
|
- app/controllers/shakha/session_controller.rb
|
|
93
|
+
- app/controllers/shakha/sessions_controller.rb
|
|
93
94
|
- app/models/shakha/session.rb
|
|
94
95
|
- app/models/shakha/user.rb
|
|
95
96
|
- app/views/shakha/auth/callback.html.erb
|