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
@@ -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 :check_rate_limit_token, only: [:token]
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 check_rate_limit_token
19
- check_rate_limit("token", max: 10, period: 1.minute)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shakha
4
- VERSION = "0.2.0"
5
- end
4
+ VERSION = "0.8.0"
5
+ end
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/middleware"
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 GoogleOAuthError < StandardError; end
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.2.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: '2.7'
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: '2.7'
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 authentication broker gem for Rails that handles Google OAuth 2.0
68
- with PKCE security. It provides domain-scoped user identifiers via pairwise subjects,
69
- ensuring the same Google account gets different IDs across different applications.
70
-
71
- Built DHH-style: database sessions (no Redis), Turbo native (zero JS), and a single
72
- "Continue with Google" button. Works as an embedded Rails engine or standalone service.
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
- - asrat@example.com
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/initializer.rb.erb
98
- - lib/generators/shakha/templates/migration.rb.erb
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.dev
116
+ homepage: https://github.com/Asrat77/shakha
113
117
  licenses:
114
118
  - MIT
115
119
  metadata:
116
- homepage_uri: https://shakha.dev
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: Headless Google OAuth broker with PKCE, pairwise subjects, and zero JavaScript
141
+ summary: SPA-first OAuth session broker for Rails one redirect, one token, done
134
142
  test_files: []
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Shakha
4
- class JwksController < ApplicationController
5
- def show
6
- render json: Shakha::JwtHandler.jwks,
7
- content_type: "application/json"
8
- end
9
- end
10
- end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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