touchque 5.0.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +105 -0
  3. data/LICENSE +21 -0
  4. data/README.md +383 -0
  5. data/lib/touchque/agent.rb +211 -0
  6. data/lib/touchque/agent_card.rb +159 -0
  7. data/lib/touchque/approvals.rb +214 -0
  8. data/lib/touchque/client.rb +81 -0
  9. data/lib/touchque/code_reply.rb +53 -0
  10. data/lib/touchque/cross_app_access.rb +204 -0
  11. data/lib/touchque/crypto.rb +215 -0
  12. data/lib/touchque/errors.rb +246 -0
  13. data/lib/touchque/flow.rb +362 -0
  14. data/lib/touchque/hosted_login.rb +401 -0
  15. data/lib/touchque/http.rb +127 -0
  16. data/lib/touchque/login_session.rb +86 -0
  17. data/lib/touchque/org_agent.rb +398 -0
  18. data/lib/touchque/rack/relay.rb +75 -0
  19. data/lib/touchque/rails/guard.rb +132 -0
  20. data/lib/touchque/rails/hosted_login.rb +143 -0
  21. data/lib/touchque/rails/sessions_controller.rb +12 -0
  22. data/lib/touchque/railtie.rb +34 -0
  23. data/lib/touchque/relay.rb +533 -0
  24. data/lib/touchque/resources/actions.rb +212 -0
  25. data/lib/touchque/resources/agents.rb +251 -0
  26. data/lib/touchque/resources/device_risk.rb +130 -0
  27. data/lib/touchque/resources/identity.rb +331 -0
  28. data/lib/touchque/resources/login.rb +325 -0
  29. data/lib/touchque/resources/mobile.rb +115 -0
  30. data/lib/touchque/resources/people.rb +237 -0
  31. data/lib/touchque/resources/qr_pairings.rb +82 -0
  32. data/lib/touchque/resources/safe_guard.rb +87 -0
  33. data/lib/touchque/resources/webhook.rb +177 -0
  34. data/lib/touchque/safe_approval.rb +283 -0
  35. data/lib/touchque/security_events.rb +162 -0
  36. data/lib/touchque/util.rb +260 -0
  37. data/lib/touchque/version.rb +5 -0
  38. data/lib/touchque.rb +124 -0
  39. metadata +83 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Touchque
4
+ module Rails
5
+ # Touchque Login in a Rails app: a controller with the sign-in routes, and a
6
+ # before_action for the pages that need a signed-in customer.
7
+ #
8
+ # # config/application.rb (or an initializer)
9
+ # config.touchque.login = { domain: "acme.login.touchque.com", client_id: "...", client_secret: "...",
10
+ # scope: "openid profile email offline_access" }
11
+ #
12
+ # # config/routes.rb
13
+ # Touchque::Rails.login_routes(self) # GET /login, /callback, /logout → Touchque::SessionsController
14
+ #
15
+ # class AccountController < ApplicationController
16
+ # include Touchque::Rails::HostedLogin
17
+ # before_action :require_touchque_sign_in
18
+ # def show = render(json: touchque_session["user"])
19
+ # end
20
+ #
21
+ # The session lives in encrypted cookies (AES-256-GCM, split when large; no session
22
+ # store needed), keyed from config.touchque.session_secret or secret_key_base. A token
23
+ # about to expire is refreshed when a refresh token is there (ask for offline_access).
24
+ module HostedLogin
25
+ SESSION_COOKIE = "tq_session"
26
+ TX_COOKIE = "tq_login_tx"
27
+
28
+ # GET /login: redirects to the tenant (returnTo, screen_hint=signup, login_hint,
29
+ # connection, organization and invitation are passed on).
30
+ def touchque_login
31
+ login = Touchque.hosted_login
32
+ auth = login.authorization_url(
33
+ redirect_uri: touchque_redirect_uri, screen_hint: params[:screen_hint] == "signup" ? "signup" : nil,
34
+ login_hint: params[:login_hint].presence, connection: params[:connection].presence,
35
+ organization: params[:organization].presence, invitation: params[:invitation].presence
36
+ )
37
+ tx = { "state" => auth["state"], "nonce" => auth["nonce"], "codeVerifier" => auth["codeVerifier"],
38
+ "returnTo" => Touchque::LoginSession.safe_return_to(params[:returnTo]) }
39
+ touchque_write_cookie(TX_COOKIE, Touchque::LoginSession.seal(tx, Touchque.login_secret, "tx", 900), 900)
40
+ redirect_to auth["url"], allow_other_host: true
41
+ end
42
+
43
+ # GET /callback: checks the sign-in and opens the session.
44
+ def touchque_callback
45
+ tx = Touchque::LoginSession.unseal(Touchque::LoginSession.read(TX_COOKIE, cookies.to_h), Touchque.login_secret, "tx")
46
+ touchque_write_cookie(TX_COOKIE, nil, 0)
47
+ return render(json: { error: "state_missing", error_description: "This sign-in expired or started in another browser" }, status: :bad_request) unless tx
48
+
49
+ tokens = Touchque.hosted_login.handle_callback(request.query_parameters, state: tx["state"], nonce: tx["nonce"],
50
+ code_verifier: tx["codeVerifier"], redirect_uri: touchque_redirect_uri)
51
+ session = touchque_session_from(tokens)
52
+ touchque_after_sign_in(session)
53
+ touchque_store_session(session)
54
+ redirect_to Touchque::LoginSession.safe_return_to(tx["returnTo"])
55
+ rescue Touchque::HostedLoginError => e
56
+ render json: { error: e.error_code, error_description: e.description }, status: :bad_request
57
+ end
58
+
59
+ # GET /logout: ends the session here and at the tenant.
60
+ def touchque_logout
61
+ session = touchque_session
62
+ touchque_store_session(nil)
63
+ redirect_to Touchque.hosted_login.logout_url(id_token_hint: session && session["idToken"],
64
+ post_logout_redirect_uri: Touchque.login_options[:post_logout_redirect_uri]),
65
+ allow_other_host: true
66
+ end
67
+
68
+ # The signed-in session ({"user" => claims, "accessToken", "expiresAt", "idToken",
69
+ # "refreshToken", "scope"}), refreshed when it is about to expire, or nil.
70
+ def touchque_session
71
+ return @touchque_session if defined?(@touchque_session)
72
+
73
+ secret = Touchque.login_secret
74
+ session = Touchque::LoginSession.unseal(Touchque::LoginSession.read(SESSION_COOKIE, cookies.to_h), secret, "session")
75
+ if session && session["expiresAt"].to_i - 30 < Touchque::Util.now
76
+ session = begin
77
+ session["refreshToken"] ? touchque_session_from(Touchque.hosted_login.refresh(session["refreshToken"]), session) : nil
78
+ rescue Touchque::HostedLoginError
79
+ nil
80
+ end
81
+ touchque_store_session(session)
82
+ end
83
+ @touchque_session = session
84
+ end
85
+
86
+ # before_action: the session, or a redirect to /login (JSON requests get 401).
87
+ def require_touchque_sign_in
88
+ return if touchque_session
89
+
90
+ if request.format.json?
91
+ render json: { error: "sign_in_required" }, status: :unauthorized
92
+ else
93
+ redirect_to "#{Touchque.login_options[:login_path] || "/login"}?returnTo=#{Touchque::Util.seg(Touchque::LoginSession.safe_return_to(request.fullpath))}"
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Override to link the user to your records before the redirect.
100
+ def touchque_after_sign_in(_session); end
101
+
102
+ def touchque_redirect_uri
103
+ configured = Touchque.login_options[:redirect_uri]
104
+ return configured if configured.to_s.match?(%r{\Ahttps?://})
105
+
106
+ "#{request.base_url}#{configured || Touchque.login_options[:callback_path] || "/callback"}"
107
+ end
108
+
109
+ def touchque_session_from(tokens, previous = nil)
110
+ {
111
+ "user" => tokens["claims"].nil? || tokens["claims"].empty? ? previous&.dig("user") : tokens["claims"],
112
+ "accessToken" => tokens["accessToken"], "expiresAt" => tokens["expiresAt"], "scope" => tokens["scope"],
113
+ "idToken" => tokens["idToken"] || previous&.dig("idToken"), "refreshToken" => tokens["refreshToken"] || previous&.dig("refreshToken")
114
+ }.compact
115
+ end
116
+
117
+ def touchque_store_session(session)
118
+ max_age = (Touchque.login_options[:session_max_age] || 7 * 86_400).to_i
119
+ sealed = session && Touchque::LoginSession.seal(session, Touchque.login_secret, "session", max_age)
120
+ Touchque::LoginSession.cookie_values(SESSION_COOKIE, sealed, cookies.to_h.keys).each do |name, value|
121
+ touchque_write_cookie(name, value, max_age)
122
+ end
123
+ @touchque_session = session
124
+ end
125
+
126
+ def touchque_write_cookie(name, value, max_age)
127
+ if value.nil?
128
+ cookies.delete(name, path: "/")
129
+ else
130
+ cookies[name] = { value: value, path: "/", httponly: true, same_site: :lax, secure: request.ssl?, expires: Time.now + max_age }
131
+ end
132
+ end
133
+ end
134
+
135
+ # The Touchque Login routes: GET /login, /callback and /logout to +controller+ (default
136
+ # "touchque/sessions": Touchque::SessionsController, provided by this gem).
137
+ def self.login_routes(router, controller: "touchque/sessions", login: "/login", callback: "/callback", logout: "/logout")
138
+ router.get login, to: "#{controller}#touchque_login"
139
+ router.get callback, to: "#{controller}#touchque_callback"
140
+ router.get logout, to: "#{controller}#touchque_logout"
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Touchque
4
+ # GET /login, /callback and /logout of Touchque Login (Touchque::Rails.login_routes).
5
+ # Subclass it (and point login_routes at yours) to link the user to your records in
6
+ # touchque_after_sign_in.
7
+ class SessionsController < ActionController::Base
8
+ include Touchque::Rails::HostedLogin
9
+
10
+ skip_forgery_protection
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module Touchque
6
+ # Rails: config.touchque (or credentials.touchque, or TQ_API_KEY / TQ_API_SECRET)
7
+ # configures Touchque.client; config.touchque.login configures Touchque Login.
8
+ #
9
+ # config.touchque.api_key = "tq_..." # default: credentials.touchque.api_key, then TQ_API_KEY
10
+ # config.touchque.api_secret = "..."
11
+ # config.touchque.login = { domain: "acme.login.touchque.com", client_id: "...", client_secret: "..." }
12
+ # config.touchque.session_secret = "..." # Touchque Login cookies; default: secret_key_base
13
+ class Railtie < ::Rails::Railtie
14
+ config.touchque = ActiveSupport::OrderedOptions.new
15
+
16
+ initializer "touchque.configure" do |app|
17
+ settings = app.config.touchque
18
+ credentials = app.respond_to?(:credentials) ? (app.credentials.config[:touchque] || {}) : {}
19
+ options = {
20
+ api_key: settings.api_key || credentials[:api_key],
21
+ api_secret: settings.api_secret || credentials[:api_secret],
22
+ base_url: settings.base_url || credentials[:base_url]
23
+ }.compact
24
+ Touchque.configure(**options) unless options.empty?
25
+ login = settings.login || credentials[:login]
26
+ Touchque.login_options = login.to_h if login
27
+ Touchque.login_secret = -> { settings.session_secret || app.secret_key_base }
28
+ end
29
+
30
+ initializer "touchque.sessions_controller" do
31
+ ActiveSupport.on_load(:action_controller_base) { require "touchque/rails/sessions_controller" }
32
+ end
33
+ end
34
+ end