shakha 0.1.1 → 0.1.3

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: 82e8c2eda1b9b817ce681d22f4e5a331d15e0c59588c972067d1fa432db03b62
4
- data.tar.gz: 979e1a3513ac57e1b1d6a81fffd954c1fb911fbb8d0cca729b65ce161bbe9f44
3
+ metadata.gz: 7ff145da09437c51afcef947199c0225b2fb557a6e0559e1792e07d7af646250
4
+ data.tar.gz: d95b3e5a110d49b7defde447935240d27e0faf2a76e3874b4b05dba7f174336e
5
5
  SHA512:
6
- metadata.gz: 80529b985eb5d490eada76a0829a9aa3311ec30af375c83b4f33ef0cb5e2399e4b1f75242bb8d5ee843e8b49d583da36ac84f53d0953b33d96868bf80dd9bb33
7
- data.tar.gz: 15777157e9a5fb3746019943f760b57ef92a9b10680056d0178f65d42392ed0391da8f1c3bf72c90e5c32d2c6110dac3420a8e1062cc0cf3d999b50309888c3d
6
+ metadata.gz: f61b03a6afdb461afde6274f2892b54f1f7979369023a1421b5037f99b333351c5c7bca5194c7f76db22d9f516594c4c737d96f501f82b8663c2ad5fafa472d1
7
+ data.tar.gz: 344441bf4fcfc2dc8061bb25fab5dfbe64485919405658cbbd16c376ffcdddc685682e0256d7e2fe2401c29e22a8122a157439e111748ccb8d6e0b8c266dc9a4
data/README.md CHANGED
@@ -1,22 +1,72 @@
1
1
  # Shakha
2
2
 
3
- Minimal auth broker for Google OAuth with PKCE and pairwise subjects.
3
+ Headless Google OAuth broker for Rails PKCE, pairwise subjects, zero JavaScript.
4
4
 
5
- ## Installation
5
+ Same Google account, different IDs per application. Built DHH-style: database sessions, Turbo native, single "Continue with Google" button.
6
6
 
7
- Add to your Gemfile:
7
+ ## Installation
8
8
 
9
9
  ```ruby
10
10
  gem "shakha"
11
11
  ```
12
12
 
13
- Run the installer:
13
+ Run the migration:
14
14
 
15
15
  ```bash
16
- rails generate shakha:install
16
+ bin/rails generate migration CreateShakhaTables
17
+ ```
18
+
19
+ ```ruby
20
+ class CreateShakhaTables < ActiveRecord::Migration[7.1]
21
+ def change
22
+ create_table :shakha_clients do |t|
23
+ t.string :name, null: false
24
+ t.string :origin, null: false
25
+ t.timestamps
26
+ t.index :origin, unique: true
27
+ end
28
+
29
+ create_table :shakha_users do |t|
30
+ t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
31
+ t.string :pairwise_sub, null: false
32
+ t.string :email
33
+ t.string :name
34
+ t.string :picture
35
+ t.timestamps
36
+ t.index :pairwise_sub, unique: true
37
+ t.index :email
38
+ end
39
+
40
+ create_table :shakha_sessions do |t|
41
+ t.references :user, foreign_key: { to_table: :shakha_users }
42
+ t.references :client, null: false, foreign_key: { to_table: :shakha_clients }
43
+ t.string :token, null: false
44
+ t.string :jti, null: false
45
+ t.timestamps
46
+ t.index :token, unique: true
47
+ t.index :jti, unique: true
48
+ t.index :created_at
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ Create `config/initializers/shakha.rb`:
57
+
58
+ ```ruby
59
+ Shakha.setup do |config|
60
+ config.app_origin = ENV.fetch("SHAKHA_APP_ORIGIN", "http://localhost:3000")
61
+ config.service_url = ENV["SHAKHA_SERVICE_URL"] # omit for embedded mode
62
+ config.service_secret = ENV["SHAKHA_SERVICE_SECRET"]
63
+ config.google_client_id = ENV["GOOGLE_CLIENT_ID"]
64
+ config.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
65
+ config.session_lifetime = 30.days
66
+ end
17
67
  ```
18
68
 
19
- Set environment variables:
69
+ Environment variables:
20
70
 
21
71
  ```bash
22
72
  export SHAKHA_APP_ORIGIN="https://yourapp.com"
@@ -25,60 +75,67 @@ export GOOGLE_CLIENT_ID="your-google-client-id"
25
75
  export GOOGLE_CLIENT_SECRET="your-google-client-secret"
26
76
  ```
27
77
 
28
- ## Configuration
29
-
30
- See `config/initializers/shakha.rb` for all options.
78
+ Google Cloud Console redirect URI: `https://yourapp.com/auth/shakha/callback`
31
79
 
32
80
  ## Usage
33
81
 
34
82
  ### Sign In
35
83
 
36
- Redirect users to sign in:
37
-
38
84
  ```erb
39
85
  <%= link_to "Sign in with Google", shakha.new_auth_path %>
40
86
  ```
41
87
 
42
- ### Current User
43
-
44
- In controllers:
88
+ ### Protect Routes
45
89
 
46
90
  ```ruby
47
91
  class ApplicationController < ActionController::Base
48
92
  include Shakha::ControllerHelpers
93
+ before_action :authenticate!
49
94
  end
50
95
  ```
51
96
 
97
+ ### Current User
98
+
52
99
  ```ruby
53
100
  current_user # Shakha::User or nil
54
- current_session # Shakha::Session or nil
55
- signed_in? # boolean
56
- authenticate! # redirect to login if not signed in
101
+ current_session # Shakha::Session or nil
102
+ signed_in? # boolean
103
+ authenticate! # redirects to login if not signed in
57
104
  ```
58
105
 
59
- ### Protect Routes
106
+ ### Sign Out
60
107
 
61
- ```ruby
62
- class PostsController < ApplicationController
63
- before_action :authenticate!
64
- end
108
+ ```erb
109
+ <%= link_to "Sign out", shakha.session_path, data: { turbo_method: :delete } %>
65
110
  ```
66
111
 
67
112
  ### JWT Verification (API Mode)
68
113
 
69
114
  ```ruby
70
115
  payload = Shakha.verify_token(id_token)
71
- user_id = payload[:sub]
116
+ user_id = payload[:pairwise_sub]
72
117
  ```
73
118
 
74
119
  ## Architecture
75
120
 
76
121
  - **PKCE** — S256 code challenges on every flow
77
- - **Pairwise subjects** — domain-scoped user identifiers
78
- - **ES256 JWTs** — signed with JWKS endpoint
122
+ - **Pairwise subjects** — domain-scoped user identifiers (HMAC-SHA256)
123
+ - **ES256 JWTs** — signed with JWKS endpoint at `.well-known/jwks.json`
124
+ - **OpenID Connect** — `.well-known/openid-configuration` endpoint
79
125
  - **Database sessions** — DHH-style, no Redis
80
- - **Turbo native** — zero JS needed
126
+ - **Turbo native** — zero JavaScript needed
127
+ - **Embedded or standalone** — runs as Rails engine or headless service
128
+
129
+ ## Modes
130
+
131
+ ### Embedded (default)
132
+
133
+ Mount in your Rails app. Routes served at `/auth/shakha`. Uses the app's own `shakha_clients` table with a single client auto-created on first request.
134
+
135
+ ### Service (multi-tenant)
136
+
137
+ Set `SHAKHA_SERVICE_URL` and register each app's origin in `shakha_clients`. Each app gets different pairwise subjects for the same Google user.
81
138
 
82
139
  ## License
83
140
 
84
- MIT
141
+ MIT
@@ -11,10 +11,11 @@ module Shakha
11
11
 
12
12
  def new
13
13
  @client = find_or_create_client
14
- @return_to = params[:return_to] || "/"
14
+ @return_to = sanitize_return_to(params[:return_to])
15
15
  end
16
16
 
17
17
  def authorize
18
+ params[:return_to] = sanitize_return_to(params[:return_to])
18
19
  pkce = create_pkce_bundle
19
20
  @client = find_or_create_client
20
21
 
@@ -27,7 +28,8 @@ module Shakha
27
28
  pkce_result = verify_pkce!(params[:code], params[:state])
28
29
  exchange_code_for_tokens(params[:code], pkce_result[:verifier], pkce_result[:return_to])
29
30
  rescue PKCEError, GoogleOAuthError => e
30
- redirect_to "/auth/shakha/error?message=#{URI.encode_www_form_component(e.message)}"
31
+ Rails.logger.warn("[Shakha] Auth error: #{e.class}: #{e.message}")
32
+ redirect_to "/auth/shakha/error?message=#{URI.encode_www_form_component(user_facing_error(e))}"
31
33
  end
32
34
 
33
35
  def token
@@ -54,13 +56,52 @@ module Shakha
54
56
 
55
57
  private
56
58
 
59
+ def sanitize_return_to(raw)
60
+ return "/" if raw.blank?
61
+
62
+ uri = URI.parse(raw)
63
+ return "/" if uri.host.present? && ![app_origin_host, client_origin_host].include?(uri.host)
64
+ return "/" unless uri.path.present? && uri.path.start_with?("/")
65
+
66
+ uri.path
67
+ rescue URI::InvalidURIError
68
+ "/"
69
+ end
70
+
71
+ def app_origin_host
72
+ URI.parse(Shakha.config.app_origin).host
73
+ end
74
+
75
+ def client_origin_host
76
+ URI.parse(Shakha.config.service_base_url).host
77
+ rescue URI::InvalidURIError
78
+ nil
79
+ end
80
+
81
+ def user_facing_error(exception)
82
+ case exception
83
+ when PKCEError
84
+ "Authentication failed. Please try again."
85
+ when GoogleOAuthError
86
+ "Unable to sign in with Google. Please try again later."
87
+ else
88
+ "An unexpected error occurred. Please try again."
89
+ end
90
+ end
91
+
57
92
  def find_or_create_client
58
93
  origin = request.origin || Shakha.config.app_origin
59
94
  origin_uri = URI.parse(origin).origin
60
95
 
61
- Shakha::Client.find_or_create_by!(origin: origin_uri) do |client|
62
- client.name = URI.parse(origin).host
96
+ if Shakha.config.embedded?
97
+ Shakha::Client.find_or_create_by!(origin: origin_uri) do |client|
98
+ client.name = URI.parse(origin).host
99
+ end
100
+ else
101
+ Shakha::Client.find_by!(origin: origin_uri)
63
102
  end
103
+ rescue ActiveRecord::RecordNotFound
104
+ raise ConfigurationError, "Unknown client origin: #{origin_uri}. Register this origin in shakha_clients first."
64
105
  end
65
106
 
66
107
  def build_google_auth_url(pkce)
@@ -114,12 +155,13 @@ module Shakha
114
155
 
115
156
  payload = decode_id_token(id_token)
116
157
  google_sub = payload["sub"]
117
- pairwise_sub = Shakha.derive_pairwise_sub(google_sub)
118
158
 
119
159
  client = find_or_create_client
160
+ pairwise_sub = Shakha.derive_pairwise_sub(google_sub, client.client_id)
161
+
120
162
  user = Shakha::User.find_or_initialize_by(pairwise_sub: pairwise_sub, client: client)
121
163
 
122
- if params[:request_pii] && payload["email"]
164
+ if payload["email"]
123
165
  user.assign_attributes(
124
166
  email: payload["email"],
125
167
  name: payload["name"],
@@ -142,7 +184,7 @@ module Shakha
142
184
  expires: Shakha.config.session_lifetime.from_now
143
185
  }
144
186
 
145
- redirect_to return_to
187
+ redirect_to sanitize_return_to(return_to)
146
188
  end
147
189
 
148
190
  def exchange_code_for_id_token(code, verifier)
@@ -27,7 +27,11 @@ module Shakha
27
27
  def destroy
28
28
  current_session&.destroy
29
29
  cookies.delete(:shakha_session_token)
30
- render json: { status: "signed_out" }
30
+
31
+ respond_to do |format|
32
+ format.html { redirect_to params[:return_to].presence || "/" }
33
+ format.json { render json: { status: "signed_out" } }
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shakha
4
+ module ConfigValidator
5
+ class << self
6
+ def validate!(config)
7
+ missing = []
8
+ missing << "SHAKHA_APP_ORIGIN" unless config.app_origin.present?
9
+ missing << "GOOGLE_CLIENT_ID" unless config.google_client_id.present?
10
+ missing << "GOOGLE_CLIENT_SECRET" unless config.google_client_secret.present?
11
+ missing << "SHAKHA_SERVICE_SECRET" unless config.service_secret.present?
12
+
13
+ unless missing.empty?
14
+ message = "Shakha: missing required configuration: #{missing.join(', ')}"
15
+ if Rails.env.production?
16
+ raise ConfigurationError, message
17
+ else
18
+ Rails.logger.warn(message)
19
+ end
20
+ end
21
+
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/shakha/engine.rb CHANGED
@@ -6,6 +6,10 @@ module Shakha
6
6
 
7
7
  config.app_middleware.use Shakha::Middleware
8
8
 
9
+ config.after_initialize do
10
+ Shakha::ConfigValidator.validate!(Shakha.config)
11
+ end
12
+
9
13
  # Engine routes - these should be relative paths
10
14
  routes do
11
15
  root to: "auth#new"
@@ -24,11 +24,12 @@ module Shakha
24
24
  end
25
25
 
26
26
  def bad_request(exception)
27
- render json: { error: exception.message }, status: :bad_request
27
+ render json: { error: "Bad request" }, status: :bad_request
28
28
  end
29
29
 
30
30
  def bad_gateway(exception)
31
- render json: { error: exception.message }, status: :bad_gateway
31
+ Rails.logger.error("[Shakha] Google OAuth error: #{exception.message}")
32
+ render json: { error: "Authentication service unavailable" }, status: :bad_gateway
32
33
  end
33
34
  end
34
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shakha
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/shakha.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "shakha/version"
4
4
  require "shakha/config"
5
+ require "shakha/config_validator"
5
6
  require "shakha/pairwise"
6
7
  require "shakha/jwt_handler"
7
8
  require "shakha/pkce"
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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Asrat
@@ -30,6 +30,9 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.1'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '10'
33
36
  type: :runtime
34
37
  prerelease: false
35
38
  version_requirements: !ruby/object:Gem::Requirement
@@ -37,9 +40,36 @@ dependencies:
37
40
  - - ">="
38
41
  - !ruby/object:Gem::Version
39
42
  version: '7.1'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '10'
46
+ - !ruby/object:Gem::Dependency
47
+ name: railties
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '7.1'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '10'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '7.1'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '10'
40
66
  description: |
41
- Shakha handles Google OAuth + PKCE and gives your app a domain-scoped identity (pairwise_sub)
42
- and a signed id_token. No client signup. No unnecessary scopes. Just identity.
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.
43
73
  email:
44
74
  - asrat@example.com
45
75
  executables: []
@@ -67,6 +97,7 @@ files:
67
97
  - lib/generators/shakha/templates/migration.rb.erb
68
98
  - lib/shakha.rb
69
99
  - lib/shakha/config.rb
100
+ - lib/shakha/config_validator.rb
70
101
  - lib/shakha/controller_helpers.rb
71
102
  - lib/shakha/engine.rb
72
103
  - lib/shakha/error_handler.rb
@@ -96,5 +127,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
127
  requirements: []
97
128
  rubygems_version: 3.6.9
98
129
  specification_version: 4
99
- summary: Minimal auth broker for Google OAuth with PKCE and pairwise subjects
130
+ summary: Headless Google OAuth broker with PKCE, pairwise subjects, and zero JavaScript
100
131
  test_files: []