shakha 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dab553b64eb62de5d3a4225c5a4aa4fb470f8c6cf4aeea518f549dd664b7e67a
4
- data.tar.gz: 0206f67ee329e747bad7da7cd3082c6be17811e19ef97ec67843d61bf50922de
3
+ metadata.gz: 82e8c2eda1b9b817ce681d22f4e5a331d15e0c59588c972067d1fa432db03b62
4
+ data.tar.gz: 979e1a3513ac57e1b1d6a81fffd954c1fb911fbb8d0cca729b65ce161bbe9f44
5
5
  SHA512:
6
- metadata.gz: 1704c398b0c6cdfa415a08f0662574eab25acb72b28ac9d8e8b83658750793111aeb9f24248ba22641ebc566500d6fbef2e63b250a895f43d6c7eb451c877d07
7
- data.tar.gz: 9eab6f79cb46672c4df6dfc26507e91518616ac67ae7119bd07bfe7a177e7488f97918c09280eafcf8192953014d264bbc5beacc9c6bd5ced23fbb2e885f1e26
6
+ metadata.gz: 80529b985eb5d490eada76a0829a9aa3311ec30af375c83b4f33ef0cb5e2399e4b1f75242bb8d5ee843e8b49d583da36ac84f53d0953b33d96868bf80dd9bb33
7
+ data.tar.gz: 15777157e9a5fb3746019943f760b57ef92a9b10680056d0178f65d42392ed0391da8f1c3bf72c90e5c32d2c6110dac3420a8e1062cc0cf3d999b50309888c3d
@@ -9,7 +9,7 @@ module Shakha
9
9
 
10
10
  layout -> { false if request.format == :json }
11
11
 
12
- rescue_from ActiveSupport::ActionController::InvalidAuthenticityToken, with: :invalid_csrf_token
12
+ rescue_from ActionController::InvalidAuthenticityToken, with: :invalid_csrf_token
13
13
 
14
14
  private
15
15
 
@@ -20,15 +20,14 @@ module Shakha
20
20
 
21
21
  google_auth_url = build_google_auth_url(pkce)
22
22
 
23
- redirect_to google_auth_url
23
+ redirect_to google_auth_url, allow_other_host: true
24
24
  end
25
25
 
26
26
  def callback
27
- verifier = verify_pkce!(params[:code])
28
-
29
- exchange_code_for_tokens(params[:code], verifier)
27
+ pkce_result = verify_pkce!(params[:code], params[:state])
28
+ exchange_code_for_tokens(params[:code], pkce_result[:verifier], pkce_result[:return_to])
30
29
  rescue PKCEError, GoogleOAuthError => e
31
- redirect_to shakha.error_path(message: e.message)
30
+ redirect_to "/auth/shakha/error?message=#{URI.encode_www_form_component(e.message)}"
32
31
  end
33
32
 
34
33
  def token
@@ -56,16 +55,18 @@ module Shakha
56
55
  private
57
56
 
58
57
  def find_or_create_client
59
- origin = URI.parse(request.origin).origin
58
+ origin = request.origin || Shakha.config.app_origin
59
+ origin_uri = URI.parse(origin).origin
60
60
 
61
- Shakha::Client.find_or_create_by!(origin: origin) do |client|
62
- client.name = URI.parse(request.origin).host
61
+ Shakha::Client.find_or_create_by!(origin: origin_uri) do |client|
62
+ client.name = URI.parse(origin).host
63
63
  end
64
64
  end
65
65
 
66
66
  def build_google_auth_url(pkce)
67
67
  client_id = Shakha.config.google_client_id || ENV["GOOGLE_CLIENT_ID"]
68
- redirect_uri = "#{Shakha.config.service_base_url}/auth/shakha/callback"
68
+ base_url = Shakha.config.service_base_url || "http://localhost:3000"
69
+ redirect_uri = "#{base_url}/auth/shakha/callback"
69
70
 
70
71
  scopes = ["openid", "email", "profile"].join(" ")
71
72
  scopes += " https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile" if params[:request_pii]
@@ -87,10 +88,11 @@ module Shakha
87
88
  end.to_s
88
89
  end
89
90
 
90
- def exchange_code_for_tokens(code, verifier)
91
+ def exchange_code_for_tokens(code, verifier, return_to = "/")
91
92
  client_id = Shakha.config.google_client_id || ENV["GOOGLE_CLIENT_ID"]
92
93
  client_secret = Shakha.config.google_client_secret || ENV["GOOGLE_CLIENT_SECRET"]
93
- redirect_uri = "#{Shakha.config.service_base_url}/auth/shakha/callback"
94
+ base_url = Shakha.config.service_base_url || "http://localhost:3000"
95
+ redirect_uri = "#{base_url}/auth/shakha/callback"
94
96
 
95
97
  response = http_post(
96
98
  "https://oauth2.googleapis.com/token",
@@ -115,7 +117,7 @@ module Shakha
115
117
  pairwise_sub = Shakha.derive_pairwise_sub(google_sub)
116
118
 
117
119
  client = find_or_create_client
118
- user = Shakha::User.find_or_initialize_by(pairwise_sub: pairwise_sub)
120
+ user = Shakha::User.find_or_initialize_by(pairwise_sub: pairwise_sub, client: client)
119
121
 
120
122
  if params[:request_pii] && payload["email"]
121
123
  user.assign_attributes(
@@ -140,8 +142,6 @@ module Shakha
140
142
  expires: Shakha.config.session_lifetime.from_now
141
143
  }
142
144
 
143
- return_to = pkce_state&.dig(:return_to) || "/"
144
-
145
145
  redirect_to return_to
146
146
  end
147
147
 
@@ -4,10 +4,11 @@ module Shakha
4
4
  class User < ::ApplicationRecord
5
5
  self.table_name = "shakha_users"
6
6
 
7
+ belongs_to :client, class_name: "Shakha::Client"
7
8
  has_many :sessions, class_name: "Shakha::Session", dependent: :destroy
8
9
 
9
- validates :pairwise_sub, presence: true, uniqueness: true
10
- validates :email, uniqueness: true, allow_blank: true
10
+ validates :pairwise_sub, presence: true
11
+ validates :email, uniqueness: { scope: :client_id }, allow_blank: true
11
12
 
12
13
  def can_access?(resource)
13
14
  true
@@ -0,0 +1,18 @@
1
+ <% content_for :title, "Authentication Error" %>
2
+
3
+ <div class="shakha-container">
4
+ <div class="shakha-card shakha-card-error">
5
+ <div class="shakha-error-icon">
6
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
7
+ <circle cx="12" cy="12" r="10"/>
8
+ <line x1="12" y1="8" x2="12" y2="12"/>
9
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
10
+ </svg>
11
+ </div>
12
+
13
+ <h1>Authentication Failed</h1>
14
+ <p class="shakha-error-message"><%= params[:message] || "An error occurred" %></p>
15
+
16
+ <%= link_to "Try Again", "/auth/shakha", class: "shakha-button shakha-button-primary" %>
17
+ </div>
18
+ </div>
@@ -1,8 +1,4 @@
1
1
  # frozen_string_literal: true
2
- # This generator creates a migration for the Shakha tables
3
-
4
- require "rails/generators/active_record/migration"
5
- require "rails/generators/active_record/migration/migration_generator"
6
2
 
7
3
  module Shakha
8
4
  class InstallGenerator < Rails::Generators::Base
@@ -17,21 +13,13 @@ module Shakha
17
13
  def create_migration
18
14
  return if options[:skip_migration]
19
15
 
20
- migration_template(
21
- "migration.rb.erb",
22
- "db/migrate/create_shakha_tables.rb",
23
- migration_version: migration_version
24
- )
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"
25
19
  end
26
20
 
27
21
  def add_routes
28
22
  route 'mount Shakha::Engine => "/auth/shakha", as: :shakha'
29
23
  end
30
-
31
- private
32
-
33
- def migration_version
34
- ">= 7.1" ? "[7.1]" : ""
35
- end
36
24
  end
37
25
  end
@@ -20,7 +20,7 @@ Shakha.setup do |config|
20
20
  config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
21
21
  config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
22
22
  config.issuer = ENV.fetch("SHAKHA_ISSUER", "https://shakha.dev")
23
- config.session_lifetime = ENV.fetch("SHAKHA_SESSION_LIFETIME", "30.days")
23
+ config.session_lifetime = 30.days
24
24
 
25
25
  # JWT signing keys (required for service mode)
26
26
  config.signing_key = ENV["SHAKHA_SIGNING_KEY"]
@@ -2,7 +2,7 @@
2
2
 
3
3
  class CreateShakhaTables < ActiveRecord::Migration[7.1]
4
4
  def change
5
- create_table :shakha_clients, id: :uuid do |t|
5
+ create_table :shakha_clients do |t|
6
6
  t.string :name, null: false
7
7
  t.string :origin, null: false
8
8
  t.string :client_id
@@ -14,12 +14,12 @@ class CreateShakhaTables < ActiveRecord::Migration[7.1]
14
14
  add_index :shakha_clients, :origin, unique: true
15
15
  add_index :shakha_clients, :client_id, unique: true
16
16
 
17
- create_table :shakha_users, id: :uuid do |t|
17
+ create_table :shakha_users do |t|
18
18
  t.string :pairwise_sub, null: false
19
19
  t.string :email
20
20
  t.string :name
21
21
  t.string :picture
22
- t.references :client, type: :uuid, null: false
22
+ t.references :client, null: false
23
23
 
24
24
  t.timestamps
25
25
  end
@@ -27,11 +27,11 @@ class CreateShakhaTables < ActiveRecord::Migration[7.1]
27
27
  add_index :shakha_users, :pairwise_sub, unique: true
28
28
  add_index :shakha_users, :email
29
29
 
30
- create_table :shakha_sessions, id: :uuid do |t|
30
+ create_table :shakha_sessions do |t|
31
31
  t.string :token, null: false
32
32
  t.string :jti, null: false
33
- t.references :user, type: :uuid, null: false
34
- t.references :client, type: :uuid, null: false
33
+ t.references :user, null: false
34
+ t.references :client, null: false
35
35
  t.string :ip_address
36
36
  t.string :user_agent
37
37
 
data/lib/shakha/config.rb CHANGED
@@ -8,7 +8,10 @@ module Shakha
8
8
  :google_client_id,
9
9
  :google_client_secret,
10
10
  :issuer,
11
- :session_lifetime
11
+ :session_lifetime,
12
+ :signing_key,
13
+ :verification_key,
14
+ :key_id
12
15
 
13
16
  def initialize
14
17
  @session_lifetime = 30.days
data/lib/shakha/engine.rb CHANGED
@@ -1,93 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ostruct"
4
-
5
3
  module Shakha
6
4
  class Engine < ::Rails::Engine
7
5
  isolate_namespace Shakha
8
6
 
9
7
  config.app_middleware.use Shakha::Middleware
10
8
 
11
- config.after_initialize do
12
- if Shakha.config.app_origin.blank?
13
- raise ConfigurationError, "Shakha.app_origin must be set"
14
- end
15
- end
16
-
17
- class EngineRouter
18
- def self.draw
19
- Drawer.new
20
- end
21
-
22
- class Drawer
23
- def initialize
24
- @routes = []
25
- end
26
-
27
- def resources(*args, &block)
28
- resource_options = args.last.is_a?(Hash) ? args.pop : {}
29
- resource_name = args.first
30
-
31
- @routes << { type: :resources, name: resource_name, options: resource_options, block: block }
32
- end
33
-
34
- def resource(*args, &block)
35
- resource_options = args.last.is_a?(Hash) ? args.pop : {}
36
- resource_name = args.first
37
-
38
- @routes << { type: :resource, name: resource_name, options: resource_options, block: block }
39
- end
40
-
41
- def get(path, to:, as: nil)
42
- @routes << { type: :get, path: path, to: to, as: as }
43
- end
44
-
45
- def post(path, to:, as: nil)
46
- @routes << { type: :post, path: path, to: to, as: as }
47
- end
48
-
49
- def match(path, to:, via:, as: nil)
50
- @routes << { type: :match, path: path, to: to, via: via, as: as }
51
- end
52
-
53
- def routes
54
- @routes
55
- end
56
- end
57
- end
58
-
59
- initializer "shakha.routes" do |app|
60
- Shakha::EngineRouter.draw do
61
- get "/auth/shakha", to: "auth#new", as: :new_auth
62
- get "/auth/shakha/authorize", to: "auth#authorize", as: :authorize
63
- get "/auth/shakha/callback", to: "auth#callback", as: :callback
64
- post "/auth/shakha/token", to: "auth#token", as: :token
65
- get "/auth/shakha/error", to: "auth#error", as: :error
9
+ # Engine routes - these should be relative paths
10
+ routes do
11
+ root to: "auth#new"
66
12
 
67
- get "/auth/shakha/session", to: "session#show", as: :session
68
- post "/auth/shakha/session/check", to: "session#check", as: :check_session
69
- delete "/auth/shakha/session", to: "session#destroy", as: :destroy_session
13
+ get "authorize" => "auth#authorize"
14
+ get "callback" => "auth#callback"
15
+ post "token" => "auth#token"
16
+ get "error" => "auth#error"
70
17
 
71
- get "/.well-known/jwks.json", to: "jwks#show"
72
- get "/.well-known/openid-configuration", to: "openid#configuration"
73
- end
18
+ get "session" => "session#show"
19
+ post "session/check" => "session#check"
20
+ delete "session" => "session#destroy"
74
21
 
75
- Shakha::EngineRouter.routes.each do |route|
76
- case route[:type]
77
- when :get
78
- app.routes.append do
79
- get route[:path], to: route[:to], as: route[:as]
80
- end
81
- when :post
82
- app.routes.append do
83
- post route[:path], to: route[:to], as: route[:as]
84
- end
85
- when :match
86
- app.routes.append do
87
- match route[:path], to: route[:to], as: route[:as], via: route[:via]
88
- end
89
- end
90
- end
22
+ get ".well-known/jwks.json" => "jwks#show"
23
+ get ".well-known/openid-configuration" => "openid#configuration"
91
24
  end
92
25
  end
93
26
  end
data/lib/shakha/pkce.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "active_support/concern"
4
5
 
5
6
  module Shakha
@@ -8,6 +9,8 @@ module Shakha
8
9
 
9
10
  CODE_VERIFIER_LENGTH = 64
10
11
  CODE_CHALLENGE_METHOD = "S256"
12
+ PKCE_COOKIE_NAME = "shakha_pkce"
13
+ PKCE_COOKIE_EXPIRY_SECONDS = 600
11
14
 
12
15
  class << self
13
16
  def generate_code_verifier
@@ -30,31 +33,56 @@ module Shakha
30
33
  verifier = PKCEMixin.generate_code_verifier
31
34
  challenge = PKCEMixin.generate_code_challenge(verifier)
32
35
  state = SecureRandom.urlsafe_base64(32)
36
+ return_to = params[:return_to] || "/"
33
37
 
34
- session[:shakha_pkce] = {
38
+ pkce_record = {
35
39
  verifier: verifier,
36
- state: state,
37
- return_to: params[:return_to] || request.referer || "/"
40
+ return_to: return_to
41
+ }
42
+
43
+ cookies[PKCE_COOKIE_NAME] = {
44
+ value: pkce_record.merge(state: state).to_json,
45
+ httponly: true,
46
+ secure: Rails.env.production?,
47
+ same_site: :lax,
48
+ expires: Time.now.utc + PKCE_COOKIE_EXPIRY_SECONDS
38
49
  }
39
50
 
40
51
  { challenge: challenge, state: state }
41
52
  end
42
53
 
43
- def verify_pkce!(code_verifier)
44
- stored = session[:shakha_pkce]
54
+ def verify_pkce!(code_verifier, state_param)
55
+ pkce_json = cookies[PKCE_COOKIE_NAME]
56
+
57
+ raise PKCEError, "No PKCE session found" unless pkce_json
58
+
59
+ pkce_data = JSON.parse(pkce_json).with_indifferent_access
60
+
61
+ raise PKCEError, "No PKCE session found" unless pkce_data
45
62
 
46
- raise PKCEError, "No PKCE session found" unless stored
47
- raise PKCEError, "State mismatch" unless stored[:state] == params[:state]
63
+ stored_state = pkce_data[:state]
64
+ stored_verifier = pkce_data[:verifier]
65
+ stored_return_to = pkce_data[:return_to]
66
+
67
+ cookies.delete(PKCE_COOKIE_NAME)
68
+
69
+ raise PKCEError, "State mismatch" unless stored_state == state_param
48
70
 
49
71
  computed = PKCEMixin.generate_code_challenge(code_verifier)
50
- raise PKCEError, "Invalid code verifier" unless computed == params[:code_challenge]
72
+ code_challenge = params[:code_challenge]
51
73
 
52
- session.delete(:shakha_pkce)
53
- stored[:verifier]
74
+ if code_challenge.present?
75
+ raise PKCEError, "Invalid code verifier" unless computed == code_challenge
76
+ end
77
+
78
+ { verifier: stored_verifier, return_to: stored_return_to }
54
79
  end
55
80
 
56
81
  def pkce_state
57
- session[:shakha_pkce]
82
+ pkce_json = cookies[PKCE_COOKIE_NAME]
83
+ return nil unless pkce_json
84
+
85
+ JSON.parse(pkce_json).with_indifferent_access
58
86
  end
59
87
  end
60
88
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shakha
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
data/lib/shakha.rb CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  require "shakha/version"
4
4
  require "shakha/config"
5
+ require "shakha/pairwise"
6
+ require "shakha/jwt_handler"
7
+ require "shakha/pkce"
8
+ require "shakha/error_handler"
9
+ require "shakha/controller_helpers"
10
+ require "shakha/middleware"
5
11
  require "shakha/engine"
6
12
 
7
13
  module Shakha
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.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -27,14 +27,14 @@ dependencies:
27
27
  name: activesupport
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.1'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '7.1'
40
40
  description: |
@@ -58,12 +58,13 @@ files:
58
58
  - app/models/shakha/session.rb
59
59
  - app/models/shakha/user.rb
60
60
  - app/views/shakha/auth/callback.html.erb
61
+ - app/views/shakha/auth/error.html.erb
61
62
  - app/views/shakha/auth/new.html.erb
62
63
  - app/views/shakha/errors/show.html.erb
63
64
  - app/views/shakha/layouts/shakha.html.erb
64
- - generators/shakha/install_generator.rb
65
- - generators/shakha/templates/initializer.rb.erb
66
- - generators/shakha/templates/migration.rb.erb
65
+ - lib/generators/shakha/install_generator.rb
66
+ - lib/generators/shakha/templates/initializer.rb.erb
67
+ - lib/generators/shakha/templates/migration.rb.erb
67
68
  - lib/shakha.rb
68
69
  - lib/shakha/config.rb
69
70
  - lib/shakha/controller_helpers.rb