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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +105 -0
- data/LICENSE +21 -0
- data/README.md +383 -0
- data/lib/touchque/agent.rb +211 -0
- data/lib/touchque/agent_card.rb +159 -0
- data/lib/touchque/approvals.rb +214 -0
- data/lib/touchque/client.rb +81 -0
- data/lib/touchque/code_reply.rb +53 -0
- data/lib/touchque/cross_app_access.rb +204 -0
- data/lib/touchque/crypto.rb +215 -0
- data/lib/touchque/errors.rb +246 -0
- data/lib/touchque/flow.rb +362 -0
- data/lib/touchque/hosted_login.rb +401 -0
- data/lib/touchque/http.rb +127 -0
- data/lib/touchque/login_session.rb +86 -0
- data/lib/touchque/org_agent.rb +398 -0
- data/lib/touchque/rack/relay.rb +75 -0
- data/lib/touchque/rails/guard.rb +132 -0
- data/lib/touchque/rails/hosted_login.rb +143 -0
- data/lib/touchque/rails/sessions_controller.rb +12 -0
- data/lib/touchque/railtie.rb +34 -0
- data/lib/touchque/relay.rb +533 -0
- data/lib/touchque/resources/actions.rb +212 -0
- data/lib/touchque/resources/agents.rb +251 -0
- data/lib/touchque/resources/device_risk.rb +130 -0
- data/lib/touchque/resources/identity.rb +331 -0
- data/lib/touchque/resources/login.rb +325 -0
- data/lib/touchque/resources/mobile.rb +115 -0
- data/lib/touchque/resources/people.rb +237 -0
- data/lib/touchque/resources/qr_pairings.rb +82 -0
- data/lib/touchque/resources/safe_guard.rb +87 -0
- data/lib/touchque/resources/webhook.rb +177 -0
- data/lib/touchque/safe_approval.rb +283 -0
- data/lib/touchque/security_events.rb +162 -0
- data/lib/touchque/util.rb +260 -0
- data/lib/touchque/version.rb +5 -0
- data/lib/touchque.rb +124 -0
- 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
|