machina-auth 0.1.2 → 0.1.4

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: 72016a1f127a752307bd3351a7179bd514a07f01257141b5976c24512f9a58ab
4
- data.tar.gz: 49e3d3a871defaf71cc93405268d6935afdbe97131b5f565fcf74fa7d336c8fa
3
+ metadata.gz: c8b85487ba807943451cf345d17e7aec7ee18a9b61a233adcdb213f86134cde0
4
+ data.tar.gz: 49908d36c4fb879354aa6c3b9ae96e33eaed76d98df2cd3dbfb8a51ce72b03a4
5
5
  SHA512:
6
- metadata.gz: 1b1ca73243c466e604fd6922c9f60ea716d264e7dac913093fdbcaca1fa4e18b5aea6318eae5d5f04fc5809644680c6b39147339f2fa95358da4e74238efee88
7
- data.tar.gz: 78582e31a57898eacd0a5abfb4c4bb812eb6683420549590a88ffbe8ffa0ded22e091cdf093f76ef3db75675719d81b4dc7de05482a7babc5d364f03e56d4b2f
6
+ metadata.gz: 97716d4c519933b65a8410615655adcf7e70a4013d9595e7ef5ca4a96ae2a57c44574640b33de5dc1adcd675b626c2ae39d09ffd0c5cf4fbcbb2932924988be9
7
+ data.tar.gz: 868b463264e64e8551c938704bcfad481714e0d6a9f4aa8dcc6152eb4d8df1bda0c6a01ff2ab55f89e534ceeb03b1d53abed3792dc2afaa1de660c6726dd2e2c
@@ -11,7 +11,8 @@ module Machina
11
11
  :cache_store,
12
12
  :cache_ttl,
13
13
  :manifest,
14
- :skip_paths
14
+ :skip_paths,
15
+ :identity_callback_uri
15
16
 
16
17
  def initialize
17
18
  @cache_ttl = 5.minutes
@@ -34,7 +34,7 @@ module Machina
34
34
  if request.format.json?
35
35
  render json: { error: 'unauthorized' }, status: :unauthorized
36
36
  else
37
- redirect_to Machina.authorize_url(redirect_to: request.original_url), allow_other_host: true
37
+ redirect_to Machina.authorize_url(return_to: request.original_url), allow_other_host: true
38
38
  end
39
39
  end
40
40
 
@@ -11,11 +11,11 @@ module Machina
11
11
 
12
12
  def call(env)
13
13
  request = ActionDispatch::Request.new(env)
14
-
14
+ # Skip paths allow the developer to disable the middleware from validating
15
+ # any token or headers on any matched URL or Path.
15
16
  return @app.call(env) if skip_path?(request)
16
17
 
17
18
  token = extract_token(request)
18
-
19
19
  return @app.call(env) if token.blank?
20
20
 
21
21
  session_data = resolve_session(token)
@@ -53,10 +53,7 @@ module Machina
53
53
  end
54
54
 
55
55
  def extract_token(request)
56
- request.cookies['machina_session'] ||
57
- extract_bearer(request) ||
58
- request.headers['X-Api-Key'] ||
59
- request.params['token']
56
+ request.cookies['machina_session'] || extract_bearer(request) || request.headers['X-Api-Key'] || request.params['token']
60
57
  end
61
58
 
62
59
  def extract_bearer(request)
@@ -76,7 +73,11 @@ module Machina
76
73
 
77
74
  def fetch_from_identity_service(token)
78
75
  response = Machina.identity_client.resolve_session(token)
79
- return nil unless response.success?
76
+
77
+ unless response.success?
78
+ Machina.cache.delete(cache_key(token))
79
+ return nil
80
+ end
80
81
 
81
82
  data = unwrap_payload(response.parsed)
82
83
  Machina.cache.write(cache_key(token), data, expires_in: Machina.config.cache_ttl)
@@ -91,6 +92,7 @@ module Machina
91
92
  def cache_workspace_ref(data)
92
93
  workspace = data['workspace']
93
94
  organization = data['organization']
95
+
94
96
  return unless workspace.is_a?(Hash) && organization.is_a?(Hash)
95
97
  return unless defined?(Machina::WorkspaceRef) && Machina::WorkspaceRef.table_exists?
96
98
 
@@ -16,7 +16,8 @@ module Machina
16
16
 
17
17
  product_id = manifest[:product_id] || Machina.config.product_id
18
18
  if product_id.blank?
19
- raise Machina::ConfigurationError, 'product_id is required for permission sync (set in machina.yml or Machina.config)'
19
+ raise Machina::ConfigurationError,
20
+ 'product_id is required for permission sync (set in machina.yml or Machina.config)'
20
21
  end
21
22
 
22
23
  Machina.identity_client.sync_permissions(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Machina
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
data/lib/machina.rb CHANGED
@@ -32,6 +32,7 @@ module Machina
32
32
 
33
33
  # Test helpers are opt-in; load with `require "machina/test_helpers"` or autoload
34
34
  autoload :TestHelpers, 'machina/test_helpers'
35
+
35
36
  class << self
36
37
  def configure
37
38
  yield(config)
@@ -54,15 +55,36 @@ module Machina
54
55
  config.cache_store || Rails.cache
55
56
  end
56
57
 
57
- def authorize_url(redirect_to:)
58
+ # Builds the Console authorize URL.
59
+ #
60
+ # @param redirect_to [String, nil] explicit redirect URL (backwards compat)
61
+ # @param return_to [String, nil] user's intended destination, appended to
62
+ # the configured +identity_callback_uri+ as a query param
63
+ # @return [String] the full authorize URL
64
+ # @raise [ConfigurationError] when neither +redirect_to+ nor
65
+ # +identity_callback_uri+ is available
66
+ def authorize_url(redirect_to: nil, return_to: nil)
58
67
  base = config.identity_service_url.to_s.sub(%r{/\z}, '')
59
- return "#{base}/authorize" if redirect_to.blank?
60
68
 
61
- "#{base}/authorize?redirect_to=#{CGI.escape(redirect_to)}"
69
+ return "#{base}/authorize?redirect_to=#{CGI.escape(redirect_to)}" if redirect_to.present?
70
+
71
+ callback = config.identity_callback_uri
72
+ if callback.blank?
73
+ raise ConfigurationError,
74
+ 'identity_callback_uri must be configured to use authorize_url without an explicit redirect_to'
75
+ end
76
+
77
+ target = return_to.present? ? "#{callback}?return_to=#{CGI.escape(return_to)}" : callback
78
+
79
+ "#{base}/authorize?redirect_to=#{CGI.escape(target)}"
62
80
  end
63
81
 
82
+ # Convenience wrapper that delegates to {authorize_url} with +return_to+.
83
+ #
84
+ # @param return_to [String] the user's intended destination
85
+ # @return [String] the full authorize URL
64
86
  def login_url(return_to:)
65
- authorize_url(redirect_to: return_to)
87
+ authorize_url(return_to:)
66
88
  end
67
89
  end
68
90
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../rails_helper'
4
+
5
+ RSpec.describe 'Machina.authorize_url' do
6
+ let(:base_url) { 'https://machina.example.test' }
7
+
8
+ context 'when identity_callback_uri is configured' do
9
+ before do
10
+ Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
11
+ end
12
+
13
+ it 'uses the callback URI as redirect_to' do
14
+ url = Machina.authorize_url
15
+
16
+ expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape('http://localhost:3000/auth/machina/callback')}")
17
+ end
18
+
19
+ it 'appends return_to as a query param on the callback URI' do
20
+ url = Machina.authorize_url(return_to: '/dashboard')
21
+
22
+ callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=%2Fdashboard'
23
+ expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
24
+ end
25
+
26
+ it 'encodes a full return_to URL' do
27
+ url = Machina.authorize_url(return_to: 'http://localhost:3000/inquiries?page=2')
28
+
29
+ callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=http%3A%2F%2Flocalhost%3A3000%2Finquiries%3Fpage%3D2'
30
+ expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
31
+ end
32
+ end
33
+
34
+ context 'when identity_callback_uri is not configured' do
35
+ it 'raises a configuration error when called without redirect_to' do
36
+ expect { Machina.authorize_url }.to raise_error(
37
+ Machina::ConfigurationError,
38
+ /identity_callback_uri/,
39
+ )
40
+ end
41
+
42
+ it 'still works with an explicit redirect_to for backwards compatibility' do
43
+ url = Machina.authorize_url(redirect_to: 'http://localhost:3000/login')
44
+
45
+ expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape('http://localhost:3000/login')}")
46
+ end
47
+ end
48
+
49
+ describe '.login_url' do
50
+ before do
51
+ Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
52
+ end
53
+
54
+ it 'delegates to authorize_url with return_to' do
55
+ url = Machina.login_url(return_to: '/settings')
56
+
57
+ callback_with_return = 'http://localhost:3000/auth/machina/callback?return_to=%2Fsettings'
58
+ expect(url).to eq("#{base_url}/authorize?redirect_to=#{CGI.escape(callback_with_return)}")
59
+ end
60
+ end
61
+ end
@@ -12,4 +12,10 @@ RSpec.describe Machina::Configuration do
12
12
  config.product_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
13
13
  expect(config.product_id).to eq('a1b2c3d4-e5f6-7890-abcd-ef1234567890')
14
14
  end
15
+
16
+ it 'supports identity_callback_uri configuration' do
17
+ config = described_class.new
18
+ config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
19
+ expect(config.identity_callback_uri).to eq('http://localhost:3000/auth/machina/callback')
20
+ end
15
21
  end
@@ -29,12 +29,15 @@ RSpec.describe Machina::ControllerHelpers, type: :controller do
29
29
  end
30
30
  end
31
31
 
32
- it 'redirects browser requests to authorize when unauthenticated' do
32
+ it 'redirects browser requests to authorize using identity_callback_uri when unauthenticated' do
33
+ Machina.config.identity_callback_uri = 'http://localhost:3000/auth/machina/callback'
34
+
33
35
  get :index
34
36
 
35
37
  expect(response).to have_http_status(:redirect)
36
38
  expect(response.location).to include('https://machina.example.test/authorize')
37
39
  expect(response.location).to include('redirect_to=')
40
+ expect(CGI.unescape(response.location)).to include('http://localhost:3000/auth/machina/callback?return_to=')
38
41
  end
39
42
 
40
43
  it 'returns json unauthorized for api-style requests' do
@@ -54,6 +54,52 @@ RSpec.describe Machina::Middleware::Authentication do
54
54
  expect(JSON.parse(body.first)).to eq('error' => 'unauthorized')
55
55
  end
56
56
 
57
+ it 'evicts cached session when Console returns non-200 on stale re-fetch' do
58
+ token = 'ps_stale_token'
59
+ cache_key = "machina:session:#{token}"
60
+ stale_data = MockResponses.session_resolution_minimal['data'].merge(stale: true)
61
+
62
+ Machina.cache.write(cache_key, stale_data, expires_in: 5.minutes)
63
+
64
+ allow(identity_client).to receive(:resolve_session).with(token).and_return(
65
+ Machina::IdentityClient::Response.new(status: 404, body: '{}'),
66
+ )
67
+
68
+ env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
69
+ status, = middleware.call(env)
70
+
71
+ expect(status).to eq(401)
72
+ expect(Machina.cache.read(cache_key)).to be_nil
73
+ end
74
+
75
+ it 'evicts cached session when Console returns non-200 after cache expires' do
76
+ token = 'ps_expired_cache'
77
+ cache_key = "machina:session:#{token}"
78
+
79
+ # First request: populate cache via successful resolution
80
+ allow(identity_client).to receive(:resolve_session).with(token).and_return(
81
+ Machina::IdentityClient::Response.new(status: 200, body: MockResponses.session_resolution_minimal),
82
+ )
83
+
84
+ env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
85
+ status, = middleware.call(env)
86
+ expect(status).to eq(200)
87
+
88
+ # Simulate cache expiry
89
+ Machina.cache.delete(cache_key)
90
+
91
+ # Console now rejects the token
92
+ allow(identity_client).to receive(:resolve_session).with(token).and_return(
93
+ Machina::IdentityClient::Response.new(status: 404, body: '{}'),
94
+ )
95
+
96
+ env = Rack::MockRequest.env_for('/resource', 'HTTP_AUTHORIZATION' => "Bearer #{token}")
97
+ status, = middleware.call(env)
98
+
99
+ expect(status).to eq(401)
100
+ expect(Machina.cache.read(cache_key)).to be_nil
101
+ end
102
+
57
103
  it 'uses the cache on subsequent requests' do
58
104
  response = Machina::IdentityClient::Response.new(
59
105
  status: 200,
@@ -106,7 +106,7 @@ RSpec.describe Machina::PermissionSync do
106
106
  context 'with an ERB manifest' do
107
107
  let(:tmpfile) do
108
108
  file = Tempfile.new(['machina', '.yml'])
109
- file.write(<<~'YAML')
109
+ file.write(<<~YAML)
110
110
  product_id: <%= "erb-product-id" %>
111
111
 
112
112
  permissions:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: machina-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ZAR
@@ -174,6 +174,7 @@ files:
174
174
  - spec/dummy/config/routes.rb
175
175
  - spec/dummy/db/schema.rb
176
176
  - spec/fixtures/machina.yml
177
+ - spec/machina/authorize_url_spec.rb
177
178
  - spec/machina/authorized_spec.rb
178
179
  - spec/machina/configuration_spec.rb
179
180
  - spec/machina/controller_helpers_spec.rb