ruby_native 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: '00576379773bb42c2d69e98cd880e81852ae7ec56c0428b13fea96c6bbc974cb'
4
- data.tar.gz: eafeb8440ec88a7418577eefaccf5251e0c658f0b3ea0ccc89da809b0aba4710
3
+ metadata.gz: b546f190679f4ae4dde09215caab504a0a1d08066a21550b0489c2b85918f04b
4
+ data.tar.gz: a13166af16459ef7d46bfa04b4527d378975928f5d69125d17060f3f66641cd0
5
5
  SHA512:
6
- metadata.gz: 8126c66d154001be50aa69da1586b5981afc3d7d1154b8f1bcf1e186320b601aad3370d8294091c528216f0c8a154b077c929e687011108c403bb6e685dd8370
7
- data.tar.gz: 628c3bef731601b5eb202ae4767f1ad7bf4610a429c1c880bc0945662e861b487c7b8123f04a44697dd9a0145f7d88cef14a66383a20d06a0f1d07226a7b31b4
6
+ metadata.gz: 945389ac999ee2fc61577ca83483169e3bc0366db35817b8f730d8e405070e657a63b0c59f08ae24b59995651eafb3cff2a3662dc571767f172ee4261781b2bb
7
+ data.tar.gz: db4a34d192c1166ee9a64ed5983a940038bd43e5e7ee19b8def64666f00e29c61ed9415d8bf405fd28abc6c761bf58be29dc175ffcc4525727c324e7cfebf2be
data/README.md CHANGED
@@ -91,6 +91,7 @@ Place helpers in the `<body>`, not the `<head>`.
91
91
  ### Any mode
92
92
 
93
93
  - `native_app?` - true when the request comes from a Ruby Native app (checks user agent)
94
+ - `native_version` - returns the app version as a `RubyNative::NativeVersion`. Defaults to `"0"` when the version is unknown. Supports string comparisons: `native_version >= "1.4"`.
94
95
  - `native_tabs_tag(enabled: true)` - shows the native tab bar.
95
96
  - `native_push_tag` - requests push notification permission.
96
97
  - `native_back_button_tag(text = nil, **options)` - renders a back button for Normal Mode. Hidden by default, shown when the native app sets `body.can-go-back`. Not needed in [Advanced Mode](https://rubynative.com/docs/advanced-mode) where the system provides a native back button.
@@ -0,0 +1,26 @@
1
+ module RubyNative
2
+ module Auth
3
+ class SessionsController < ::ActionController::Base
4
+ def show
5
+ data = OAuthMiddleware.read_token(params[:token])
6
+
7
+ unless data
8
+ Rails.logger.debug { "[RubyNative] OAuth token exchange failed: invalid or expired token" }
9
+ head :unauthorized
10
+ return
11
+ end
12
+
13
+ # Prevent the session middleware from appending its own (empty)
14
+ # session cookie, which would overwrite the authenticated one.
15
+ request.session_options[:skip] = true
16
+
17
+ if data[:cookies].present?
18
+ response.headers["set-cookie"] = data[:cookies].join("\n")
19
+ end
20
+
21
+ Rails.logger.debug { "[RubyNative] OAuth token exchanged, redirecting to #{data[:redirect_url]}" }
22
+ render json: {redirect_url: data[:redirect_url]}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ module RubyNative
2
+ module Auth
3
+ class StartController < ::ActionController::Base
4
+ def show
5
+ @provider = params[:provider]
6
+
7
+ unless @provider.match?(/\A[a-z0-9_]+\z/)
8
+ head :bad_request
9
+ return
10
+ end
11
+
12
+ @callback_scheme = params[:callback_scheme]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Signing in…</title>
7
+ </head>
8
+ <body>
9
+ <p>Signing in…</p>
10
+ <%= form_tag "/auth/#{@provider}", method: :post, id: "oauth-form" do %>
11
+ <input type="hidden" name="ruby_native" value="1">
12
+ <input type="hidden" name="callback_scheme" value="<%= @callback_scheme %>">
13
+ <% end %>
14
+ <script>document.getElementById("oauth-form").submit();</script>
15
+ </body>
16
+ </html>
data/config/routes.rb CHANGED
@@ -3,4 +3,8 @@ RubyNative::Engine.routes.draw do
3
3
  namespace :push do
4
4
  resources :devices, only: :create
5
5
  end
6
+ namespace :auth do
7
+ get "start/:provider", to: "start#show", as: :start
8
+ resource :session, only: :show
9
+ end
6
10
  end
@@ -28,6 +28,10 @@ module RubyNative
28
28
  end
29
29
  end
30
30
 
31
+ initializer "ruby_native.oauth_middleware" do |app|
32
+ app.middleware.insert_before ActionDispatch::Cookies, RubyNative::OAuthMiddleware
33
+ end
34
+
31
35
  initializer "ruby_native.routes" do |app|
32
36
  app.routes.prepend do
33
37
  mount RubyNative::Engine, at: "/native"
@@ -1,9 +1,5 @@
1
1
  module RubyNative
2
2
  module Helper
3
- def native_app?
4
- request.user_agent.to_s.include?("Ruby Native")
5
- end
6
-
7
3
  def native_tabs_tag(enabled: true)
8
4
  safe_join([
9
5
  (tag.div(data: { native_tabs: true }, hidden: true) if enabled),
@@ -2,8 +2,17 @@ module RubyNative
2
2
  module NativeDetection
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ helper_method :native_app?, :native_version if respond_to?(:helper_method)
7
+ end
8
+
5
9
  def native_app?
6
10
  request.user_agent.to_s.include?("Ruby Native")
7
11
  end
12
+
13
+ def native_version
14
+ match = request.user_agent.to_s.match(/Ruby Native.*?\/([\d.]+)/)
15
+ NativeVersion.new(match ? match[1] : "0")
16
+ end
8
17
  end
9
18
  end
@@ -0,0 +1,144 @@
1
+ module RubyNative
2
+ class OAuthMiddleware
3
+ COOKIE_NAME = "_ruby_native_oauth"
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ request = ActionDispatch::Request.new(env)
11
+ started_oauth = oauth_start_request?(request)
12
+ callback_scheme = request.params["callback_scheme"] if started_oauth
13
+
14
+ status, headers, body = @app.call(env)
15
+
16
+ if started_oauth && callback_scheme.present? && redirect?(status)
17
+ Rails.logger.debug { "[RubyNative] OAuth started for #{request.path}, setting tracking cookie" }
18
+ set_cookie(headers, callback_scheme)
19
+ end
20
+
21
+ stored_scheme = read_cookie(request)
22
+
23
+ if stored_scheme && redirect?(status)
24
+ location = headers["location"] || headers["Location"]
25
+
26
+ if auth_failure?(location)
27
+ Rails.logger.info { "[RubyNative] OAuth failed, redirecting to native app" }
28
+ delete_cookie(headers)
29
+ return redirect_to_native(stored_scheme, error: true)
30
+ end
31
+
32
+ if internal_redirect?(request, location)
33
+ token = build_token(headers, location)
34
+ Rails.logger.info { "[RubyNative] OAuth succeeded, redirecting to native app" }
35
+ delete_cookie(headers)
36
+ return redirect_to_native(stored_scheme, token: token)
37
+ end
38
+ end
39
+
40
+ [status, headers, body]
41
+ end
42
+
43
+ def self.encryptor
44
+ @encryptor ||= begin
45
+ key = ActiveSupport::KeyGenerator.new(Rails.application.secret_key_base)
46
+ .generate_key("ruby_native_oauth", ActiveSupport::MessageEncryptor.key_len)
47
+ ActiveSupport::MessageEncryptor.new(key)
48
+ end
49
+ end
50
+
51
+ def self.build_token(cookies:, redirect_url:)
52
+ encryptor.encrypt_and_sign(
53
+ {cookies: cookies, redirect_url: redirect_url},
54
+ expires_in: 5.minutes,
55
+ purpose: "ruby_native_oauth"
56
+ )
57
+ end
58
+
59
+ def self.read_token(token)
60
+ data = encryptor.decrypt_and_verify(token, purpose: "ruby_native_oauth")
61
+ data&.symbolize_keys
62
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
63
+ nil
64
+ end
65
+
66
+ private
67
+
68
+ def oauth_start_request?(request)
69
+ return false unless oauth_paths.any? { |p| request.path == p }
70
+ request.params["ruby_native"] == "1"
71
+ end
72
+
73
+ def set_cookie(headers, callback_scheme)
74
+ signed = verifier.generate(callback_scheme)
75
+ Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, {
76
+ value: signed,
77
+ path: "/",
78
+ httponly: true,
79
+ secure: Rails.env.production?,
80
+ same_site: :lax,
81
+ max_age: 300
82
+ })
83
+ end
84
+
85
+ def read_cookie(request)
86
+ signed_value = request.cookies[COOKIE_NAME]
87
+ return nil unless signed_value.present?
88
+ verifier.verified(signed_value)
89
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
90
+ nil
91
+ end
92
+
93
+ def delete_cookie(headers)
94
+ Rack::Utils.delete_set_cookie_header!(headers, COOKIE_NAME, path: "/")
95
+ end
96
+
97
+ def verifier
98
+ @verifier ||= ActiveSupport::MessageVerifier.new(
99
+ Rails.application.secret_key_base,
100
+ digest: "SHA256",
101
+ purpose: "ruby_native_oauth"
102
+ )
103
+ end
104
+
105
+ def redirect?(status)
106
+ (300..399).cover?(status)
107
+ end
108
+
109
+ def auth_failure?(location)
110
+ return false unless location
111
+ URI.parse(location).path == "/auth/failure"
112
+ rescue URI::InvalidURIError
113
+ false
114
+ end
115
+
116
+ def internal_redirect?(request, location)
117
+ return false unless location
118
+ uri = URI.parse(location)
119
+ uri.host.nil? || uri.host == request.host
120
+ rescue URI::InvalidURIError
121
+ false
122
+ end
123
+
124
+ def build_token(headers, redirect_url)
125
+ raw_cookies = headers["set-cookie"] || headers["Set-Cookie"]
126
+ cookies = case raw_cookies
127
+ when String then raw_cookies.split("\n")
128
+ when Array then raw_cookies
129
+ else []
130
+ end
131
+
132
+ self.class.build_token(cookies: cookies, redirect_url: redirect_url)
133
+ end
134
+
135
+ def redirect_to_native(callback_scheme, token: nil, error: false)
136
+ query = error ? "error=true" : "token=#{CGI.escape(token)}"
137
+ [302, {"location" => "#{callback_scheme}://auth/callback?#{query}"}, [""]]
138
+ end
139
+
140
+ def oauth_paths
141
+ RubyNative.config&.dig(:auth, :oauth_paths) || []
142
+ end
143
+ end
144
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyNative
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.4"
3
3
  end
data/lib/ruby_native.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "ruby_native/version"
2
2
  require "ruby_native/helper"
3
+ require "ruby_native/native_version"
3
4
  require "ruby_native/native_detection"
5
+ require "ruby_native/oauth_middleware"
4
6
  require "ruby_native/engine"
5
7
 
6
8
  module RubyNative
@@ -13,5 +15,6 @@ module RubyNative
13
15
  self.config = YAML.load_file(path).deep_symbolize_keys
14
16
  self.config[:app] ||= {}
15
17
  self.config[:app][:name] ||= "Ruby Native"
18
+ self.config[:auth] ||= {}
16
19
  end
17
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_native
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
  - Joe Masilotti
@@ -49,6 +49,8 @@ files:
49
49
  - LICENSE
50
50
  - README.md
51
51
  - app/assets/stylesheets/ruby_native.css
52
+ - app/controllers/ruby_native/auth/sessions_controller.rb
53
+ - app/controllers/ruby_native/auth/start_controller.rb
52
54
  - app/controllers/ruby_native/config_controller.rb
53
55
  - app/controllers/ruby_native/push/devices_controller.rb
54
56
  - app/javascript/ruby_native/back.js
@@ -59,6 +61,7 @@ files:
59
61
  - app/javascript/ruby_native/bridge/push_controller.js
60
62
  - app/javascript/ruby_native/bridge/search_controller.js
61
63
  - app/javascript/ruby_native/bridge/tabs_controller.js
64
+ - app/views/ruby_native/auth/start/show.html.erb
62
65
  - config/importmap.rb
63
66
  - config/routes.rb
64
67
  - exe/ruby_native
@@ -71,6 +74,7 @@ files:
71
74
  - lib/ruby_native/engine.rb
72
75
  - lib/ruby_native/helper.rb
73
76
  - lib/ruby_native/native_detection.rb
77
+ - lib/ruby_native/oauth_middleware.rb
74
78
  - lib/ruby_native/version.rb
75
79
  homepage: https://github.com/Ruby-Native/gem
76
80
  licenses: