shakha 0.3.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.
@@ -5,12 +5,13 @@ module Shakha
5
5
  self.table_name = "shakha_sessions"
6
6
 
7
7
  belongs_to :user, class_name: "Shakha::User", optional: true
8
- belongs_to :client, class_name: "Shakha::Client"
9
8
 
10
9
  before_create :generate_token
11
10
 
12
11
  scope :active, -> { where("created_at > ?", Shakha.config.session_lifetime.ago) }
13
12
 
13
+ EXCHANGE_CODE_TTL = 60.seconds
14
+
14
15
  def expired?
15
16
  created_at < Shakha.config.session_lifetime.ago
16
17
  end
@@ -19,10 +20,34 @@ module Shakha
19
20
  created_at + Shakha.config.session_lifetime
20
21
  end
21
22
 
23
+ # Issues a short-lived, single-use code the frontend swaps for the session
24
+ # token, so the token itself never travels in a redirect URL.
25
+ def generate_exchange_code!
26
+ update_columns(
27
+ exchange_code: SecureRandom.urlsafe_base64(32),
28
+ exchange_code_expires_at: EXCHANGE_CODE_TTL.from_now
29
+ )
30
+ exchange_code
31
+ end
32
+
33
+ # Redeems a code for its session, atomically clearing it so a code can be
34
+ # used at most once even under concurrent requests.
35
+ def self.exchange(code)
36
+ return nil if code.blank?
37
+
38
+ session = active.where(exchange_code: code)
39
+ .where("exchange_code_expires_at > ?", Time.current).first
40
+ return nil unless session
41
+
42
+ claimed = where(id: session.id).where.not(exchange_code: nil)
43
+ .update_all(exchange_code: nil, exchange_code_expires_at: nil)
44
+ claimed == 1 ? session : nil
45
+ end
46
+
22
47
  private
23
48
 
24
49
  def generate_token
25
50
  self.token ||= SecureRandom.urlsafe_base64(32)
26
51
  end
27
52
  end
28
- end
53
+ end
@@ -4,12 +4,10 @@ module Shakha
4
4
  class User < ::ApplicationRecord
5
5
  self.table_name = "shakha_users"
6
6
 
7
- belongs_to :client, class_name: "Shakha::Client"
8
7
  has_many :sessions, class_name: "Shakha::Session", dependent: :destroy
9
8
 
10
9
  validates :provider, presence: true
11
10
  validates :uid, presence: true
12
11
  validates :uid, uniqueness: { scope: :provider }
13
- validates :email, uniqueness: { scope: :client_id }, allow_blank: true
14
12
  end
15
- end
13
+ end
@@ -1,4 +1,4 @@
1
- <% content_for :title, "Sign in — #{@client&.name || 'Shakha'}" %>
1
+ <% content_for :title, "Sign in — #{Shakha.config.app_name}" %>
2
2
 
3
3
  <div class="sh-layout sh-animate-fade">
4
4
  <div class="sh-card sh-animate-slide">
@@ -6,18 +6,18 @@
6
6
  <div class="sh-brand">
7
7
  <div class="sh-brand__icon">S</div>
8
8
  <div>
9
- <div class="sh-brand__name"><%= @client&.name || "Shakha" %></div>
9
+ <div class="sh-brand__name"><%= Shakha.config.app_name %></div>
10
10
  <div class="sh-brand__subtitle">Secure authentication</div>
11
11
  </div>
12
12
  </div>
13
13
 
14
14
  <h1 class="sh-heading sh-heading--center">Welcome back</h1>
15
- <p class="sh-subheading">Sign in to continue to <%= @client&.name || "your app" %></p>
15
+ <p class="sh-subheading">Sign in to continue to <%= Shakha.config.app_name %></p>
16
16
  </div>
17
17
 
18
18
  <div class="sh-card__body">
19
19
  <% @providers.each do |provider| %>
20
- <%= link_to shakha.send("#{provider}_authorize_path"),
20
+ <%= link_to "/auth/shakha/#{provider}",
21
21
  class: "sh-btn sh-btn--#{provider}",
22
22
  data: { turbo: false } do %>
23
23
  Continue with <%= provider.to_s.titleize %>
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title><%= yield(:title).presence || "Shakha" %></title>
7
- <%= stylesheet_link_tag "shakha", "data-turbo-track": "reload" %>
7
+ <style><%= Shakha::Engine.root.join("app/assets/stylesheets/shakha.css").read.html_safe %></style>
8
8
  <%= csrf_meta_tags %>
9
9
  <meta name="theme-color" content="#0f0f23" media="(prefers-color-scheme: dark)">
10
10
  <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record"
4
+
5
+ module Shakha
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include ActiveRecord::Generators::Migration
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ desc "Installs Shakha — headless OAuth broker for Rails"
12
+
13
+ def copy_migration
14
+ migration_template "create_shakha_tables.rb.erb", "db/migrate/create_shakha_tables.rb"
15
+ end
16
+
17
+ def copy_initializer
18
+ template "shakha.rb.erb", "config/initializers/shakha.rb"
19
+ end
20
+
21
+ def inject_application_controller
22
+ path = "app/controllers/application_controller.rb"
23
+ full_path = File.join(destination_root, path)
24
+ return unless File.exist?(full_path)
25
+ return if File.read(full_path).include?("Shakha::ControllerHelpers")
26
+
27
+ inject_into_class path, "ApplicationController", " include Shakha::ControllerHelpers\n"
28
+ say_status :insert, "ApplicationController -> include Shakha::ControllerHelpers", :green
29
+ end
30
+
31
+ def enable_cookies_for_api_mode
32
+ return unless api_only_app?
33
+
34
+ application "config.middleware.use ActionDispatch::Cookies"
35
+ say_status :insert, "config/application.rb -> ActionDispatch::Cookies (API mode)", :green
36
+ end
37
+
38
+ def print_post_install
39
+ origin = Shakha.config.app_origin || "http://localhost:3000"
40
+
41
+ say ""
42
+ say " Shakha installed!", :green
43
+ say " #{'─' * 50}", :green
44
+ say ""
45
+ say " 1. Set environment variables:", :yellow
46
+ say " GOOGLE_CLIENT_ID", :cyan
47
+ say " GOOGLE_CLIENT_SECRET", :cyan
48
+ say ""
49
+ say " 2. (Optional) GitHub:", :yellow
50
+ say " GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET", :cyan
51
+ say ""
52
+ say " 3. For SPA: set ALLOWED_REDIRECT_ORIGINS", :yellow
53
+ say ""
54
+ say " 4. Run migrations:", :yellow
55
+ say " bin/rails db:migrate", :cyan
56
+ say ""
57
+ say " 5. Google Cloud Console redirect URI:", :yellow
58
+ say " #{origin}/auth/shakha/google/callback", :cyan
59
+ say ""
60
+ say " 6. GitHub OAuth App callback URL:", :yellow
61
+ say " #{origin}/auth/shakha/github/callback", :cyan
62
+ say " #{'─' * 50}", :green
63
+ say ""
64
+ say " Tell your frontend dev:", :cyan
65
+ say " Sign in: #{origin}/auth/shakha/google"
66
+ say " Session: GET #{origin}/auth/shakha/session"
67
+ say " Auth: Authorization: Bearer <token>"
68
+ say " Sign out: DELETE #{origin}/auth/shakha/sign_out"
69
+ say ""
70
+ end
71
+
72
+ private
73
+
74
+ def migration_version
75
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
76
+ end
77
+
78
+ def api_only_app?
79
+ path = File.join(destination_root, "config/application.rb")
80
+ File.exist?(path) && File.read(path).include?("config.api_only = true")
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ class CreateShakhaTables < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :shakha_users do |t|
4
+ t.string :provider, null: false
5
+ t.string :uid, null: false
6
+ t.string :email
7
+ t.string :name
8
+ t.string :picture
9
+ t.timestamps
10
+ t.index [:provider, :uid], unique: true
11
+ t.index :email
12
+ end
13
+
14
+ create_table :shakha_sessions do |t|
15
+ t.references :user, foreign_key: { to_table: :shakha_users }
16
+ t.string :token, null: false
17
+ t.string :exchange_code
18
+ t.datetime :exchange_code_expires_at
19
+ t.string :ip_address
20
+ t.string :user_agent
21
+ t.timestamps
22
+ t.index :token, unique: true
23
+ t.index :exchange_code, unique: true
24
+ t.index :created_at
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ Shakha.setup do |config|
4
+ # Your Rails app's origin
5
+ config.app_origin = ENV.fetch("APP_ORIGIN", "http://localhost:3000")
6
+
7
+ # Name shown on the built-in sign-in page (Rails monolith flow)
8
+ # config.app_name = "My App"
9
+
10
+ # Allowed frontend origins for the SPA redirect
11
+ config.allowed_redirect_origins = ENV.fetch("ALLOWED_REDIRECT_ORIGINS", "").split(",")
12
+
13
+ # How the session token reaches the frontend after OAuth (default:
14
+ # :exchange_code). With :exchange_code the callback redirects with a
15
+ # one-time ?code=..., which the frontend swaps for the token:
16
+ # POST /auth/shakha/session/exchange { "code": "..." }
17
+ # -> { "token": "...", "expires_at": "..." }
18
+ # Set :token to put the token directly in the redirect URL instead
19
+ # (simpler, but the token is exposed in history/logs/Referer).
20
+ # config.redirect_token_delivery = :token
21
+
22
+ # Google OAuth (required)
23
+ config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
24
+ config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
25
+
26
+ # GitHub OAuth (optional — remove from providers if unused)
27
+ config.github_client_id = ENV["GITHUB_CLIENT_ID"]
28
+ config.github_client_secret = ENV["GITHUB_CLIENT_SECRET"]
29
+
30
+ # Enabled providers
31
+ # config.providers = [:google] # Google only
32
+ config.providers = [:google, :github] # Both
33
+
34
+ # Session lifetime (default: 30 days)
35
+ # config.session_lifetime = 30.days
36
+
37
+ # Rate limiting (default: false)
38
+ # config.rate_limiting_enabled = true
39
+ end
data/lib/shakha/config.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Shakha
4
4
  class Config
5
5
  attr_accessor :app_origin,
6
+ :app_name,
6
7
  :google_client_id,
7
8
  :google_client_secret,
8
9
  :github_client_id,
@@ -10,12 +11,15 @@ module Shakha
10
11
  :providers,
11
12
  :session_lifetime,
12
13
  :rate_limiting_enabled,
13
- :allowed_redirect_origins
14
+ :allowed_redirect_origins,
15
+ :redirect_token_delivery
14
16
 
15
17
  def initialize
18
+ @app_name = "Shakha"
16
19
  @session_lifetime = 30.days
17
20
  @rate_limiting_enabled = false
18
- @providers = [:google]
21
+ @providers = [ :google ]
22
+ @redirect_token_delivery = :exchange_code
19
23
  end
20
24
  end
21
25
  end
@@ -7,7 +7,7 @@ module Shakha
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- helper_method :current_user, :current_session, :signed_in?
10
+ helper_method :current_session, :current_user, :signed_in? if respond_to?(:helper_method)
11
11
  end
12
12
 
13
13
  private
@@ -28,13 +28,15 @@ module Shakha
28
28
  def authenticate!
29
29
  return if signed_in?
30
30
 
31
- respond_to do |format|
32
- format.html { redirect_to shakha.new_auth_path(return_to: request.fullpath) }
33
- format.json { render json: { error: "Authentication required" }, status: :unauthorized }
31
+ if request.format.json?
32
+ render json: { error: "Authentication required" }, status: :unauthorized
33
+ else
34
+ redirect_to "/auth/shakha?return_to=#{CGI.escape(request.fullpath)}"
34
35
  end
35
36
  end
36
37
 
37
38
  def find_session_from_cookie
39
+ return unless respond_to?(:cookies)
38
40
  token = cookies.encrypted[:shakha_session_token]
39
41
  return unless token
40
42
  Shakha::Session.active.find_by(token: token)
data/lib/shakha/engine.rb CHANGED
@@ -11,13 +11,16 @@ module Shakha
11
11
  routes do
12
12
  root to: "auth#new"
13
13
 
14
- get ":provider/authorize" => "auth#authorize"
15
- get ":provider/callback" => "auth#callback"
16
- delete "sign_out" => "auth#destroy"
17
- get "error" => "auth#error"
14
+ # Static routes MUST come before dynamic :provider routes
15
+ get "session" => "session#show"
16
+ get "session/check" => "session#check"
17
+ post "session/exchange" => "session#exchange"
18
+ delete "sign_out" => "auth#destroy"
19
+ get "error" => "auth#error"
18
20
 
19
- get "session" => "session#show"
20
- get "session/check" => "session#check"
21
+ # Dynamic provider routes
22
+ get ":provider" => "auth#authorize", as: :authorize
23
+ get ":provider/callback" => "auth#callback", as: :callback
21
24
  end
22
25
  end
23
- end
26
+ end
@@ -8,6 +8,7 @@ module Shakha
8
8
 
9
9
  included do
10
10
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
11
+ rescue_from Shakha::ProviderNotFound, with: :provider_not_found
11
12
  rescue_from Shakha::PKCEError, with: :bad_request
12
13
  rescue_from Shakha::OAuthError, with: :bad_gateway
13
14
  end
@@ -18,6 +19,10 @@ module Shakha
18
19
  render json: { error: exception.message }, status: :not_found
19
20
  end
20
21
 
22
+ def provider_not_found(_exception)
23
+ render json: { error: "Unknown provider" }, status: :not_found
24
+ end
25
+
21
26
  def unauthorized(exception)
22
27
  render json: { error: "Unauthorized" }, status: :unauthorized
23
28
  end
@@ -31,4 +36,4 @@ module Shakha
31
36
  render json: { error: "Authentication service unavailable" }, status: :bad_gateway
32
37
  end
33
38
  end
34
- end
39
+ end
data/lib/shakha/pkce.rb CHANGED
@@ -14,7 +14,7 @@ module Shakha
14
14
 
15
15
  class << self
16
16
  def generate_code_verifier
17
- SecureRandom.urlsafe_base64(CODE_VERIFIER_LENGTH, padding: false)
17
+ SecureRandom.urlsafe_base64(CODE_VERIFIER_LENGTH, false)
18
18
  end
19
19
 
20
20
  def generate_code_challenge(verifier)
@@ -32,7 +32,9 @@ module Shakha
32
32
  challenge = PKCEMixin.generate_code_challenge(verifier)
33
33
  state = SecureRandom.urlsafe_base64(32)
34
34
  nonce = SecureRandom.urlsafe_base64(32)
35
- return_to = params[:return_to] || "/"
35
+ # sanitize_return_to is provided by the including controller (AuthController);
36
+ # validating here means the stored return_to is always safe to redirect to.
37
+ return_to = sanitize_return_to(params[:return_to])
36
38
 
37
39
  pkce_record = {
38
40
  verifier: verifier,
@@ -81,5 +83,4 @@ module Shakha
81
83
  end
82
84
 
83
85
  class PKCEError < StandardError; end
84
- class GoogleOAuthError < StandardError; end
85
- end
86
+ end
@@ -3,7 +3,7 @@
3
3
  module Shakha
4
4
  module Providers
5
5
  class Base
6
- def authorize_url(state:, code_challenge:, redirect_uri:)
6
+ def authorize_url(state:, code_challenge:, redirect_uri:, nonce: nil)
7
7
  raise NotImplementedError
8
8
  end
9
9
 
@@ -11,7 +11,7 @@ module Shakha
11
11
  raise NotImplementedError
12
12
  end
13
13
 
14
- def identity_from_response(token_response)
14
+ def identity_from_response(token_response, expected_nonce: nil)
15
15
  raise NotImplementedError
16
16
  end
17
17
 
@@ -14,7 +14,9 @@ module Shakha
14
14
  :github
15
15
  end
16
16
 
17
- def authorize_url(state:, code_challenge:, redirect_uri:)
17
+ # GitHub OAuth has no nonce concept; the keyword is accepted for a
18
+ # uniform provider interface and ignored.
19
+ def authorize_url(state:, code_challenge:, redirect_uri:, nonce: nil)
18
20
  params = {
19
21
  client_id: Shakha.config.github_client_id,
20
22
  redirect_uri: redirect_uri,
@@ -36,7 +38,7 @@ module Shakha
36
38
  JSON.parse(response.body)
37
39
  end
38
40
 
39
- def identity_from_response(token_response)
41
+ def identity_from_response(token_response, expected_nonce: nil)
40
42
  access_token = token_response["access_token"]
41
43
  raise OAuthError, "No access_token received" unless access_token
42
44
 
@@ -13,7 +13,7 @@ module Shakha
13
13
  :google
14
14
  end
15
15
 
16
- def authorize_url(state:, code_challenge:, redirect_uri:)
16
+ def authorize_url(state:, code_challenge:, redirect_uri:, nonce: nil)
17
17
  params = {
18
18
  client_id: Shakha.config.google_client_id,
19
19
  redirect_uri: redirect_uri,
@@ -24,7 +24,7 @@ module Shakha
24
24
  state: state,
25
25
  access_type: "offline",
26
26
  prompt: "consent",
27
- nonce: SecureRandom.urlsafe_base64(32)
27
+ nonce: nonce
28
28
  }
29
29
 
30
30
  "#{AUTHORIZE_URL}?#{URI.encode_www_form(params)}"
@@ -43,11 +43,15 @@ module Shakha
43
43
  JSON.parse(response.body)
44
44
  end
45
45
 
46
- def identity_from_response(token_response)
46
+ def identity_from_response(token_response, expected_nonce: nil)
47
47
  id_token = token_response["id_token"]
48
48
  raise OAuthError, "No id_token received" unless id_token
49
49
 
50
+ # Signature is not re-verified: the token comes directly from Google's
51
+ # token endpoint over a TLS connection we initiated, so its provenance
52
+ # is the transport. The claims below still need checking.
50
53
  payload = JWT.decode(id_token, nil, false)[0]
54
+ verify_claims!(payload, expected_nonce)
51
55
 
52
56
  {
53
57
  provider: :google,
@@ -64,6 +68,25 @@ module Shakha
64
68
 
65
69
  private
66
70
 
71
+ VALID_ISSUERS = [ "https://accounts.google.com", "accounts.google.com" ].freeze
72
+
73
+ def verify_claims!(payload, expected_nonce)
74
+ unless VALID_ISSUERS.include?(payload["iss"])
75
+ raise OAuthError, "ID token issuer mismatch"
76
+ end
77
+
78
+ unless payload["aud"] == Shakha.config.google_client_id
79
+ raise OAuthError, "ID token audience mismatch"
80
+ end
81
+
82
+ raise OAuthError, "ID token expired" if payload["exp"].to_i <= Time.now.to_i
83
+
84
+ if expected_nonce.present? &&
85
+ !ActiveSupport::SecurityUtils.secure_compare(payload["nonce"].to_s, expected_nonce)
86
+ raise OAuthError, "ID token nonce mismatch"
87
+ end
88
+ end
89
+
67
90
  def http_post(url, body)
68
91
  uri = URI.parse(url)
69
92
  http = Net::HTTP.new(uri.host, uri.port)
@@ -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.3.0"
5
- end
4
+ VERSION = "0.8.0"
5
+ end
data/lib/shakha.rb CHANGED
@@ -24,4 +24,5 @@ module Shakha
24
24
  class ConfigurationError < StandardError; end
25
25
  class PKCEError < StandardError; end
26
26
  class OAuthError < StandardError; end
27
- 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.3.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,25 +70,26 @@ 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
92
  - app/controllers/shakha/session_controller.rb
85
- - app/models/shakha/client.rb
86
93
  - app/models/shakha/session.rb
87
94
  - app/models/shakha/user.rb
88
95
  - app/views/shakha/auth/callback.html.erb
@@ -90,9 +97,9 @@ files:
90
97
  - app/views/shakha/auth/new.html.erb
91
98
  - app/views/shakha/errors/show.html.erb
92
99
  - app/views/shakha/layouts/shakha.html.erb
93
- - lib/generators/shakha/install_generator.rb
94
- - lib/generators/shakha/templates/initializer.rb.erb
95
- - 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
96
103
  - lib/shakha.rb
97
104
  - lib/shakha/config.rb
98
105
  - lib/shakha/config_validator.rb
@@ -106,11 +113,15 @@ files:
106
113
  - lib/shakha/providers/google.rb
107
114
  - lib/shakha/rate_limiter.rb
108
115
  - lib/shakha/version.rb
109
- homepage: https://shakha.dev
116
+ homepage: https://github.com/Asrat77/shakha
110
117
  licenses:
111
118
  - MIT
112
119
  metadata:
113
- 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'
114
125
  rdoc_options: []
115
126
  require_paths:
116
127
  - lib
@@ -127,5 +138,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
138
  requirements: []
128
139
  rubygems_version: 3.6.9
129
140
  specification_version: 4
130
- 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
131
142
  test_files: []
@@ -1,16 +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 self.find_by_origin!(origin)
13
- find_by!(origin: URI.parse(origin).origin)
14
- end
15
- end
16
- end