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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/README.md +153 -92
- data/SECURITY.md +56 -0
- data/app/controllers/shakha/application_controller.rb +3 -4
- data/app/controllers/shakha/auth_controller.rb +103 -196
- data/app/controllers/shakha/session_controller.rb +27 -59
- data/app/models/shakha/session.rb +27 -7
- data/app/models/shakha/user.rb +4 -8
- data/app/views/shakha/auth/new.html.erb +9 -21
- data/app/views/shakha/layouts/shakha.html.erb +1 -1
- data/lib/generators/shakha/install/install_generator.rb +84 -0
- data/lib/generators/shakha/install/templates/create_shakha_tables.rb.erb +27 -0
- data/lib/generators/shakha/install/templates/shakha.rb.erb +39 -0
- data/lib/shakha/config.rb +10 -30
- data/lib/shakha/config_validator.rb +1 -2
- data/lib/shakha/controller_helpers.rb +16 -33
- data/lib/shakha/engine.rb +10 -17
- data/lib/shakha/error_handler.rb +8 -4
- data/lib/shakha/pkce.rb +5 -4
- data/lib/shakha/providers/base.rb +27 -0
- data/lib/shakha/providers/github.rb +101 -0
- data/lib/shakha/providers/google.rb +113 -0
- data/lib/shakha/providers.rb +19 -0
- data/lib/shakha/rate_limiter.rb +5 -5
- data/lib/shakha/version.rb +2 -2
- data/lib/shakha.rb +4 -29
- metadata +34 -26
- data/app/controllers/shakha/jwks_controller.rb +0 -10
- data/app/controllers/shakha/openid_controller.rb +0 -21
- data/app/models/shakha/client.rb +0 -20
- data/app/views/shakha/auth/sessions.html.erb +0 -66
- 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
- data/lib/shakha/auditable.rb +0 -47
- data/lib/shakha/jwt_handler.rb +0 -127
- data/lib/shakha/middleware.rb +0 -49
- data/lib/shakha/pairwise.rb +0 -26
data/lib/shakha/rate_limiter.rb
CHANGED
|
@@ -5,8 +5,8 @@ module Shakha
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
-
before_action :check_rate_limit_authorize, only: [:authorize]
|
|
9
|
-
before_action :
|
|
8
|
+
before_action :check_rate_limit_authorize, only: [ :authorize ]
|
|
9
|
+
before_action :check_rate_limit_callback, only: [ :callback ]
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
private
|
|
@@ -15,15 +15,15 @@ module Shakha
|
|
|
15
15
|
check_rate_limit("authorize", max: 20, period: 1.minute)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
check_rate_limit("
|
|
18
|
+
def check_rate_limit_callback
|
|
19
|
+
check_rate_limit("callback", max: 10, period: 1.minute)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def check_rate_limit(key, max:, period:)
|
|
23
23
|
return unless Shakha.config.rate_limiting_enabled
|
|
24
24
|
|
|
25
25
|
cache_key = "shakha-rate:#{key}:#{request.remote_ip}"
|
|
26
|
-
count = Rails.cache.increment(cache_key, 1, expires_in: period.seconds)
|
|
26
|
+
count = Rails.cache.increment(cache_key, 1, expires_in: period.seconds) || 1
|
|
27
27
|
|
|
28
28
|
if count > max
|
|
29
29
|
render json: { error: "Too many requests. Try again later." }, status: :too_many_requests
|
data/lib/shakha/version.rb
CHANGED
data/lib/shakha.rb
CHANGED
|
@@ -3,14 +3,11 @@
|
|
|
3
3
|
require "shakha/version"
|
|
4
4
|
require "shakha/config"
|
|
5
5
|
require "shakha/config_validator"
|
|
6
|
-
require "shakha/pairwise"
|
|
7
|
-
require "shakha/jwt_handler"
|
|
8
6
|
require "shakha/pkce"
|
|
9
7
|
require "shakha/rate_limiter"
|
|
10
|
-
require "shakha/auditable"
|
|
11
8
|
require "shakha/error_handler"
|
|
12
9
|
require "shakha/controller_helpers"
|
|
13
|
-
require "shakha/
|
|
10
|
+
require "shakha/providers"
|
|
14
11
|
require "shakha/engine"
|
|
15
12
|
|
|
16
13
|
module Shakha
|
|
@@ -22,32 +19,10 @@ module Shakha
|
|
|
22
19
|
def config
|
|
23
20
|
@config ||= Config.new
|
|
24
21
|
end
|
|
25
|
-
|
|
26
|
-
def verify_token(id_token, audience: nil)
|
|
27
|
-
JwtHandler.verify(id_token, audience: audience || default_audience)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def sign_token(payload, exp: 24.hours.from_now)
|
|
31
|
-
JwtHandler.encode(payload, exp: exp)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def derive_pairwise_sub(google_sub, client_id = nil)
|
|
35
|
-
Pairwise.derive(google_sub, client_id || default_client_id)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def default_audience
|
|
41
|
-
"origin:#{config.app_origin&.then { |url| URI.parse(url).origin }}"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def default_client_id
|
|
45
|
-
"origin:#{URI.parse(config.app_origin).origin}"
|
|
46
|
-
end
|
|
47
22
|
end
|
|
48
23
|
|
|
49
24
|
class ConfigurationError < StandardError; end
|
|
50
|
-
class JWTError < StandardError; end
|
|
51
25
|
class PKCEError < StandardError; end
|
|
52
|
-
class
|
|
53
|
-
end
|
|
26
|
+
class OAuthError < StandardError; end
|
|
27
|
+
class ProviderNotFound < StandardError; end
|
|
28
|
+
end
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Asrat
|
|
@@ -13,16 +13,22 @@ dependencies:
|
|
|
13
13
|
name: jwt
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 2.10.3
|
|
19
|
+
- - "<"
|
|
17
20
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
21
|
+
version: '3'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
|
-
- - "
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 2.10.3
|
|
29
|
+
- - "<"
|
|
24
30
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
31
|
+
version: '3'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: activesupport
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -64,56 +70,58 @@ dependencies:
|
|
|
64
70
|
- !ruby/object:Gem::Version
|
|
65
71
|
version: '10'
|
|
66
72
|
description: |
|
|
67
|
-
Shakha is a headless
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
Shakha is a headless OAuth broker engine for Rails APIs and monoliths.
|
|
74
|
+
Your frontend does a single redirect; Shakha runs the OAuth dance
|
|
75
|
+
(PKCE, state) against Google or GitHub, stores a revocable
|
|
76
|
+
database-backed session, and redirects back with a session token usable
|
|
77
|
+
via encrypted cookie or Authorization: Bearer. No JWTs issued, no Redis,
|
|
78
|
+
no frontend SDK. Pre-1.0: APIs and security posture still evolving.
|
|
73
79
|
email:
|
|
74
|
-
-
|
|
80
|
+
- asratextras77@gmail.com
|
|
75
81
|
executables: []
|
|
76
82
|
extensions: []
|
|
77
83
|
extra_rdoc_files: []
|
|
78
84
|
files:
|
|
85
|
+
- CHANGELOG.md
|
|
79
86
|
- LICENSE.txt
|
|
80
87
|
- README.md
|
|
88
|
+
- SECURITY.md
|
|
81
89
|
- app/assets/stylesheets/shakha.css
|
|
82
90
|
- app/controllers/shakha/application_controller.rb
|
|
83
91
|
- app/controllers/shakha/auth_controller.rb
|
|
84
|
-
- app/controllers/shakha/jwks_controller.rb
|
|
85
|
-
- app/controllers/shakha/openid_controller.rb
|
|
86
92
|
- app/controllers/shakha/session_controller.rb
|
|
87
|
-
- app/models/shakha/client.rb
|
|
88
93
|
- app/models/shakha/session.rb
|
|
89
94
|
- app/models/shakha/user.rb
|
|
90
95
|
- app/views/shakha/auth/callback.html.erb
|
|
91
96
|
- app/views/shakha/auth/error.html.erb
|
|
92
97
|
- app/views/shakha/auth/new.html.erb
|
|
93
|
-
- app/views/shakha/auth/sessions.html.erb
|
|
94
98
|
- app/views/shakha/errors/show.html.erb
|
|
95
99
|
- app/views/shakha/layouts/shakha.html.erb
|
|
96
|
-
- lib/generators/shakha/install_generator.rb
|
|
97
|
-
- lib/generators/shakha/templates/
|
|
98
|
-
- lib/generators/shakha/templates/
|
|
100
|
+
- lib/generators/shakha/install/install_generator.rb
|
|
101
|
+
- lib/generators/shakha/install/templates/create_shakha_tables.rb.erb
|
|
102
|
+
- lib/generators/shakha/install/templates/shakha.rb.erb
|
|
99
103
|
- lib/shakha.rb
|
|
100
|
-
- lib/shakha/auditable.rb
|
|
101
104
|
- lib/shakha/config.rb
|
|
102
105
|
- lib/shakha/config_validator.rb
|
|
103
106
|
- lib/shakha/controller_helpers.rb
|
|
104
107
|
- lib/shakha/engine.rb
|
|
105
108
|
- lib/shakha/error_handler.rb
|
|
106
|
-
- lib/shakha/jwt_handler.rb
|
|
107
|
-
- lib/shakha/middleware.rb
|
|
108
|
-
- lib/shakha/pairwise.rb
|
|
109
109
|
- lib/shakha/pkce.rb
|
|
110
|
+
- lib/shakha/providers.rb
|
|
111
|
+
- lib/shakha/providers/base.rb
|
|
112
|
+
- lib/shakha/providers/github.rb
|
|
113
|
+
- lib/shakha/providers/google.rb
|
|
110
114
|
- lib/shakha/rate_limiter.rb
|
|
111
115
|
- lib/shakha/version.rb
|
|
112
|
-
homepage: https://shakha
|
|
116
|
+
homepage: https://github.com/Asrat77/shakha
|
|
113
117
|
licenses:
|
|
114
118
|
- MIT
|
|
115
119
|
metadata:
|
|
116
|
-
homepage_uri: https://shakha
|
|
120
|
+
homepage_uri: https://github.com/Asrat77/shakha
|
|
121
|
+
source_code_uri: https://github.com/Asrat77/shakha
|
|
122
|
+
changelog_uri: https://github.com/Asrat77/shakha/blob/master/CHANGELOG.md
|
|
123
|
+
bug_tracker_uri: https://github.com/Asrat77/shakha/issues
|
|
124
|
+
rubygems_mfa_required: 'true'
|
|
117
125
|
rdoc_options: []
|
|
118
126
|
require_paths:
|
|
119
127
|
- lib
|
|
@@ -130,5 +138,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
130
138
|
requirements: []
|
|
131
139
|
rubygems_version: 3.6.9
|
|
132
140
|
specification_version: 4
|
|
133
|
-
summary:
|
|
141
|
+
summary: SPA-first OAuth session broker for Rails — one redirect, one token, done
|
|
134
142
|
test_files: []
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
class OpenidController < ApplicationController
|
|
5
|
-
def configuration
|
|
6
|
-
render json: {
|
|
7
|
-
issuer: Shakha.config.issuer,
|
|
8
|
-
authorization_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/authorize",
|
|
9
|
-
token_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/token",
|
|
10
|
-
userinfo_endpoint: "#{Shakha.config.service_base_url}/auth/shakha/session",
|
|
11
|
-
jwks_uri: "#{Shakha.config.service_base_url}/.well-known/jwks.json",
|
|
12
|
-
response_types_supported: ["code"],
|
|
13
|
-
grant_types_supported: ["authorization_code"],
|
|
14
|
-
code_challenge_methods_supported: ["S256"],
|
|
15
|
-
subject_types_supported: ["pairwise"],
|
|
16
|
-
id_token_signing_alg_values_supported: ["ES256"],
|
|
17
|
-
scopes_supported: ["openid", "email", "profile"]
|
|
18
|
-
}, content_type: "application/json"
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
data/app/models/shakha/client.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
class Client < ::ApplicationRecord
|
|
5
|
-
self.table_name = "shakha_clients"
|
|
6
|
-
|
|
7
|
-
has_many :sessions, class_name: "Shakha::Session", dependent: :restrict_with_error
|
|
8
|
-
has_many :users, class_name: "Shakha::User", dependent: :nullify
|
|
9
|
-
|
|
10
|
-
validates :origin, presence: true, uniqueness: true
|
|
11
|
-
|
|
12
|
-
def client_id
|
|
13
|
-
"origin:#{origin}"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.find_by_origin!(origin)
|
|
17
|
-
find_by!(origin: URI.parse(origin).origin)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<% content_for :title, "Active Sessions" %>
|
|
2
|
-
|
|
3
|
-
<div class="sh-layout sh-animate-fade">
|
|
4
|
-
<div class="sh-card sh-animate-slide" style="max-width: 560px;">
|
|
5
|
-
<div class="sh-card__header">
|
|
6
|
-
<div class="sh-brand">
|
|
7
|
-
<div class="sh-brand__icon">S</div>
|
|
8
|
-
<div>
|
|
9
|
-
<div class="sh-brand__name">Active Sessions</div>
|
|
10
|
-
<div class="sh-brand__subtitle">Manage your signed-in devices</div>
|
|
11
|
-
</div>
|
|
12
|
-
</div>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<div class="sh-sessions">
|
|
16
|
-
<div class="sh-sessions__header">
|
|
17
|
-
<span class="sh-sessions__title">Devices</span>
|
|
18
|
-
<span class="sh-sessions__count"><%= @sessions&.size || 0 %> active</span>
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<% if @sessions&.any? %>
|
|
22
|
-
<% @sessions.each do |session| %>
|
|
23
|
-
<div class="sh-session <%= 'sh-session--current' if session.token == @current_token %>">
|
|
24
|
-
<div class="sh-session__info">
|
|
25
|
-
<div style="display: flex; align-items: center; gap: var(--sh-space-2);">
|
|
26
|
-
<% if session.token == @current_token %>
|
|
27
|
-
<span class="sh-session__badge">Current</span>
|
|
28
|
-
<% end %>
|
|
29
|
-
<span class="sh-session__device">Web Browser</span>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="sh-session__meta">
|
|
32
|
-
<%= session.ip_address || "Unknown IP" %> ·
|
|
33
|
-
<%= session.created_at.strftime("%b %d, %Y at %I:%M %p") %>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<% if session.token != @current_token %>
|
|
38
|
-
<%= button_to revoke_session_path(session.id),
|
|
39
|
-
method: :delete,
|
|
40
|
-
class: "sh-session__btn",
|
|
41
|
-
data: { turbo: false, confirm: "Revoke this session?" } do %>
|
|
42
|
-
Revoke
|
|
43
|
-
<% end %>
|
|
44
|
-
<% end %>
|
|
45
|
-
</div>
|
|
46
|
-
<% end %>
|
|
47
|
-
<% else %>
|
|
48
|
-
<div class="sh-empty">
|
|
49
|
-
<div class="sh-empty__icon">
|
|
50
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
51
|
-
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
52
|
-
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
53
|
-
</svg>
|
|
54
|
-
</div>
|
|
55
|
-
<p>No active sessions found.</p>
|
|
56
|
-
</div>
|
|
57
|
-
<% end %>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div class="sh-card__footer">
|
|
61
|
-
<%= link_to "/", class: "sh-link" do %>
|
|
62
|
-
← Back to app
|
|
63
|
-
<% end %>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
class InstallGenerator < Rails::Generators::Base
|
|
5
|
-
source_root File.expand_path("templates", __dir__)
|
|
6
|
-
|
|
7
|
-
class_option :skip_migration, type: :boolean, default: false, desc: "Skip migration generation"
|
|
8
|
-
|
|
9
|
-
def copy_initializer
|
|
10
|
-
template "initializer.rb.erb", "config/initializers/shakha.rb"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def create_migration
|
|
14
|
-
return if options[:skip_migration]
|
|
15
|
-
|
|
16
|
-
sleep 1
|
|
17
|
-
migration_number = Time.now.strftime("%Y%m%d%H%M%S")
|
|
18
|
-
template "migration.rb.erb", "db/migrate/#{migration_number}_create_shakha_tables.rb"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def add_routes
|
|
22
|
-
route 'mount Shakha::Engine => "/auth/shakha", as: :shakha'
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Shakha Auth Configuration
|
|
4
|
-
#
|
|
5
|
-
# Set these environment variables:
|
|
6
|
-
# SHAKHA_SERVICE_SECRET - Secret key for pairwise subject derivation (HMAC-SHA256)
|
|
7
|
-
# GOOGLE_CLIENT_ID - Google OAuth client ID
|
|
8
|
-
# GOOGLE_CLIENT_SECRET - Google OAuth client secret
|
|
9
|
-
# SHAKHA_APP_ORIGIN - Your application's origin (e.g., http://localhost:3000)
|
|
10
|
-
# SHAKHA_SERVICE_URL - Optional: URL of standalone Shakha service (omit for embedded mode)
|
|
11
|
-
#
|
|
12
|
-
# For ES256 JWT signing, set either:
|
|
13
|
-
# SHAKHA_SIGNING_KEY - EC P-256 private key in PEM or base64 format
|
|
14
|
-
# SHAKHA_KEY_ID - Key ID for JWKS (optional, defaults to "default")
|
|
15
|
-
|
|
16
|
-
Shakha.setup do |config|
|
|
17
|
-
config.app_origin = ENV.fetch("SHAKHA_APP_ORIGIN", "http://localhost:3000")
|
|
18
|
-
config.service_url = ENV["SHAKHA_SERVICE_URL"]
|
|
19
|
-
config.service_secret = ENV["SHAKHA_SERVICE_SECRET"]
|
|
20
|
-
config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
21
|
-
config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
22
|
-
config.issuer = ENV.fetch("SHAKHA_ISSUER", "https://shakha.dev")
|
|
23
|
-
config.session_lifetime = 30.days
|
|
24
|
-
|
|
25
|
-
# JWT signing keys (required for service mode)
|
|
26
|
-
config.signing_key = ENV["SHAKHA_SIGNING_KEY"]
|
|
27
|
-
config.verification_key = ENV["SHAKHA_VERIFICATION_KEY"]
|
|
28
|
-
config.key_id = ENV["SHAKHA_KEY_ID"] || "default"
|
|
29
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateShakhaTables < ActiveRecord::Migration[7.1]
|
|
4
|
-
def change
|
|
5
|
-
create_table :shakha_clients do |t|
|
|
6
|
-
t.string :name, null: false
|
|
7
|
-
t.string :origin, null: false
|
|
8
|
-
t.string :client_id
|
|
9
|
-
t.json :metadata, default: {}
|
|
10
|
-
|
|
11
|
-
t.timestamps
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
add_index :shakha_clients, :origin, unique: true
|
|
15
|
-
add_index :shakha_clients, :client_id, unique: true
|
|
16
|
-
|
|
17
|
-
create_table :shakha_users do |t|
|
|
18
|
-
t.string :pairwise_sub, null: false
|
|
19
|
-
t.string :email
|
|
20
|
-
t.string :name
|
|
21
|
-
t.string :picture
|
|
22
|
-
t.references :client, null: false
|
|
23
|
-
|
|
24
|
-
t.timestamps
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
add_index :shakha_users, :pairwise_sub, unique: true
|
|
28
|
-
add_index :shakha_users, :email
|
|
29
|
-
|
|
30
|
-
create_table :shakha_sessions do |t|
|
|
31
|
-
t.string :token, null: false
|
|
32
|
-
t.string :jti, null: false
|
|
33
|
-
t.references :user, null: false
|
|
34
|
-
t.references :client, null: false
|
|
35
|
-
t.string :ip_address
|
|
36
|
-
t.string :user_agent
|
|
37
|
-
|
|
38
|
-
t.timestamps
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
add_index :shakha_sessions, :token, unique: true
|
|
42
|
-
add_index :shakha_sessions, :jti, unique: true
|
|
43
|
-
add_index :shakha_sessions, :created_at
|
|
44
|
-
end
|
|
45
|
-
end
|
data/lib/shakha/auditable.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
module Auditable
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
included do
|
|
8
|
-
after_action :log_sign_in
|
|
9
|
-
after_action :log_sign_out
|
|
10
|
-
after_action :log_token_exchange
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
private
|
|
14
|
-
|
|
15
|
-
def log_sign_in
|
|
16
|
-
return unless action_name == "callback" && response.successful? && @current_user
|
|
17
|
-
|
|
18
|
-
ActiveSupport::Notifications.instrument("shakha.sign_in", {
|
|
19
|
-
user_id: @current_user&.id,
|
|
20
|
-
pairwise_sub: @current_user&.pairwise_sub,
|
|
21
|
-
client_id: @current_client&.id,
|
|
22
|
-
ip: request.remote_ip,
|
|
23
|
-
user_agent: request.user_agent
|
|
24
|
-
})
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def log_sign_out
|
|
28
|
-
return unless action_name == "destroy"
|
|
29
|
-
|
|
30
|
-
ActiveSupport::Notifications.instrument("shakha.sign_out", {
|
|
31
|
-
session_id: @current_session&.id,
|
|
32
|
-
user_id: @current_session&.user_id,
|
|
33
|
-
ip: request.remote_ip
|
|
34
|
-
})
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def log_token_exchange
|
|
38
|
-
return unless action_name == "token"
|
|
39
|
-
|
|
40
|
-
ActiveSupport::Notifications.instrument("shakha.token_exchange", {
|
|
41
|
-
ip: request.remote_ip,
|
|
42
|
-
user_agent: request.user_agent,
|
|
43
|
-
success: response.successful?
|
|
44
|
-
})
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
data/lib/shakha/jwt_handler.rb
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "jwt"
|
|
4
|
-
|
|
5
|
-
module Shakha
|
|
6
|
-
class ConfigurationError < StandardError; end
|
|
7
|
-
class JWTError < StandardError; end
|
|
8
|
-
|
|
9
|
-
class JwtHandler
|
|
10
|
-
ALGORITHM = "ES256"
|
|
11
|
-
|
|
12
|
-
class << self
|
|
13
|
-
def encode(payload, exp: 24.hours.from_now)
|
|
14
|
-
secret = signing_key || raise(ConfigurationError, "RSA/EC private key required for signing")
|
|
15
|
-
|
|
16
|
-
header = {
|
|
17
|
-
alg: ALGORITHM,
|
|
18
|
-
typ: "JWT",
|
|
19
|
-
kid: key_id
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
payload = payload.with_indifferent_access.merge(
|
|
23
|
-
iss: Shakha.config.issuer,
|
|
24
|
-
aud: Shakha.config.audience,
|
|
25
|
-
iat: Time.current.to_i,
|
|
26
|
-
exp: exp.to_i,
|
|
27
|
-
jti: SecureRandom.uuid
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
JWT.encode(payload, secret, ALGORITHM, header)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def verify(token, audience: nil)
|
|
34
|
-
public_key = verification_key || raise(ConfigurationError, "RSA/EC public key required for verification")
|
|
35
|
-
|
|
36
|
-
decoded = JWT.decode(
|
|
37
|
-
token,
|
|
38
|
-
public_key,
|
|
39
|
-
true,
|
|
40
|
-
{
|
|
41
|
-
algorithm: ALGORITHM,
|
|
42
|
-
iss: Shakha.config.issuer,
|
|
43
|
-
aud: audience || Shakha.config.audience,
|
|
44
|
-
verify_iss: true,
|
|
45
|
-
verify_aud: true,
|
|
46
|
-
verify_expiration: true
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
decoded[0].with_indifferent_access
|
|
51
|
-
rescue JWT::DecodeError => e
|
|
52
|
-
raise JWTError, e.message
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def jwks
|
|
56
|
-
{
|
|
57
|
-
keys: [
|
|
58
|
-
{
|
|
59
|
-
kty: "EC",
|
|
60
|
-
crv: "P-256",
|
|
61
|
-
x: Base64.urlsafe_encode64(public_key_point&.x || public_key_raw_point[0..31], padding: false),
|
|
62
|
-
y: Base64.urlsafe_encode64(public_key_point&.y || public_key_raw_point[32..63], padding: false),
|
|
63
|
-
use: "sig",
|
|
64
|
-
alg: ALGORITHM,
|
|
65
|
-
kid: key_id
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
}.to_json
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def signing_key
|
|
74
|
-
return @signing_key if defined?(@signing_key)
|
|
75
|
-
|
|
76
|
-
key_material = Shakha.config.signing_key
|
|
77
|
-
return @signing_key = nil unless key_material
|
|
78
|
-
|
|
79
|
-
if key_material.is_a?(OpenSSL::PKey::EC)
|
|
80
|
-
@signing_key = key_material
|
|
81
|
-
elsif key_material.start_with?("-----BEGIN")
|
|
82
|
-
@signing_key = OpenSSL::PKey::EC.new(key_material)
|
|
83
|
-
else
|
|
84
|
-
@signing_key = OpenSSL::PKey::EC.new(Base64.decode64(key_material))
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def verification_key
|
|
89
|
-
return signing_key&.public_key if signing_key
|
|
90
|
-
|
|
91
|
-
public_material = Shakha.config.verification_key
|
|
92
|
-
return nil unless public_materiall
|
|
93
|
-
|
|
94
|
-
if public_material.start_with?("-----BEGIN")
|
|
95
|
-
OpenSSL::PKey::EC.new(public_material)
|
|
96
|
-
else
|
|
97
|
-
OpenSSL::PKey::EC.new(Base64.decode64(public_material))
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def public_key_point
|
|
102
|
-
@public_key_point ||= begin
|
|
103
|
-
key = verification_key || signing_key&.public_key
|
|
104
|
-
return nil unless key
|
|
105
|
-
|
|
106
|
-
group = key.group
|
|
107
|
-
point = key.public_key
|
|
108
|
-
{ x: point.x, y: point.y }
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def public_key_raw_point
|
|
113
|
-
@public_key_raw_point ||= begin
|
|
114
|
-
key = verification_key || signing_key&.public_key
|
|
115
|
-
return nil unless key
|
|
116
|
-
|
|
117
|
-
point = key.public_key
|
|
118
|
-
[point.x, point.y].map { |n| n.to_s(16).rjust(64, "0") }.join.scan(/../).map { |b| b.to_i(16).chr }.join
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def key_id
|
|
123
|
-
Shakha.config.key_id || "default"
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
data/lib/shakha/middleware.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Shakha
|
|
4
|
-
class Middleware
|
|
5
|
-
def initialize(app)
|
|
6
|
-
@app = app
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def call(env)
|
|
10
|
-
@env = env
|
|
11
|
-
@request = ActionDispatch::Request.new(env)
|
|
12
|
-
|
|
13
|
-
if verify_token_request?
|
|
14
|
-
verify_token!
|
|
15
|
-
else
|
|
16
|
-
@app.call(env)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
attr_reader :request
|
|
23
|
-
|
|
24
|
-
def verify_token_request?
|
|
25
|
-
request.path == "/auth/shakha/token" && request.post?
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def verify_token!
|
|
29
|
-
token = extract_token || raise(JWTError, "Missing token")
|
|
30
|
-
payload = Shakha.verify_token(token)
|
|
31
|
-
|
|
32
|
-
@env["shakha.user_id"] = payload[:pairwise_sub]
|
|
33
|
-
@env["shakha.email"] = payload[:email]
|
|
34
|
-
@env["shakha.name"] = payload[:name]
|
|
35
|
-
|
|
36
|
-
@app.call(@env)
|
|
37
|
-
rescue JWTError => e
|
|
38
|
-
[401, { "Content-Type" => "application/json" }, [{ error: e.message }.to_json]]
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def extract_token
|
|
42
|
-
if request.content_type == "application/json"
|
|
43
|
-
JSON.parse(request.body.read)["id_token"]
|
|
44
|
-
elsif request.params["id_token"].present?
|
|
45
|
-
request.params["id_token"]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|