strongroom-rails 0.1.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.
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strongroom
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class InvalidRequest < Error; end
7
+ class Unauthorized < Error; end
8
+ class ProtocolError < Error; end
9
+ class Unavailable < Error; end
10
+
11
+ class RateLimited < Error
12
+ attr_reader :retry_after
13
+
14
+ def initialize(retry_after: nil)
15
+ @retry_after = retry_after
16
+ super("authentication rate limited")
17
+ end
18
+ end
19
+
20
+ class Busy < RateLimited; end
21
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "uri"
6
+ require "ipaddr"
7
+
8
+ module Strongroom
9
+ TransportResponse = Data.define(:status, :headers, :body) do
10
+ def initialize(status:, headers:, body:)
11
+ code = if status.is_a?(Integer)
12
+ status
13
+ elsif status.is_a?(String) && status.match?(/\A[1-5][0-9]{2}\z/)
14
+ Integer(status, 10)
15
+ end
16
+ raise ProtocolError, "invalid authentication response status" unless code && (100..599).cover?(code)
17
+ normalized = headers.to_h do |key, values|
18
+ [key.downcase.freeze, Array(values).map { |value| value.dup.freeze }.freeze]
19
+ end
20
+ super(status: code, headers: normalized.freeze, body: body.dup.freeze)
21
+ end
22
+
23
+ def inspect = "#<Strongroom::TransportResponse status=#{status} [FILTERED]>"
24
+ alias to_s inspect
25
+ def pretty_print(printer) = printer.text(inspect)
26
+ def as_json(*) = {status: status}
27
+
28
+ def to_h
29
+ redacted = headers.to_h do |key, values|
30
+ [key, %w[set-cookie cookie authorization].include?(key) ? ["[FILTERED]"] : values]
31
+ end
32
+ {status: status, headers: redacted, body: "[FILTERED]"}
33
+ end
34
+
35
+ def encode_with(coder)
36
+ coder.map = to_h.transform_keys(&:to_s)
37
+ end
38
+
39
+ def init_with(_coder)
40
+ raise TypeError, "transport responses must not be deserialized"
41
+ end
42
+
43
+ def marshal_dump
44
+ raise TypeError, "transport responses must not be serialized"
45
+ end
46
+ end
47
+
48
+ class NetHTTPTransport
49
+ class Connection < Net::HTTP
50
+ module DeadlineReads
51
+ attr_accessor :connection
52
+
53
+ private
54
+
55
+ def rbuf_fill
56
+ self.read_timeout = connection.budget(connection.read_timeout)
57
+ super
58
+ end
59
+ end
60
+
61
+ attr_accessor :expires
62
+
63
+ def budget(limit)
64
+ remaining = expires - Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ raise Net::ReadTimeout unless remaining.positive?
66
+ [remaining, limit].min
67
+ end
68
+
69
+ private
70
+
71
+ def on_connect
72
+ @socket.extend(DeadlineReads)
73
+ @socket.connection = self
74
+ end
75
+ end
76
+
77
+ MAX_RESPONSE_BYTES = 65_536
78
+ REQUESTS = {get: Net::HTTP::Get, post: Net::HTTP::Post, delete: Net::HTTP::Delete}.freeze
79
+ FAILURES = [IOError, SystemCallError, Timeout::Error, SocketError, OpenSSL::SSL::SSLError,
80
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError].freeze
81
+ attr_reader :endpoint
82
+
83
+ def self.endpoint(value)
84
+ unless value.is_a?(String) && value.bytesize <= 2048 && !value.match?(/[[:space:]\\@]/)
85
+ raise ConfigurationError, "invalid authentication endpoint"
86
+ end
87
+ uri = URI.parse(value)
88
+ unless %w[https http].include?(uri.scheme) && uri.hostname && !uri.hostname.include?("*") &&
89
+ (1..65_535).cover?(uri.port) && !uri.userinfo &&
90
+ uri.query.nil? && uri.fragment.nil? && ["", "/"].include?(uri.path) &&
91
+ (uri.scheme == "https" || loopback?(uri.hostname))
92
+ raise ConfigurationError, "HTTPS required (HTTP is loopback-only)"
93
+ end
94
+ uri.normalize.to_s.delete_suffix("/").freeze
95
+ rescue URI::InvalidURIError, ArgumentError, TypeError
96
+ raise ConfigurationError, "invalid authentication endpoint", cause: nil
97
+ end
98
+
99
+ def self.loopback?(hostname)
100
+ hostname == "localhost" || IPAddr.new(hostname).loopback?
101
+ rescue IPAddr::Error
102
+ false
103
+ end
104
+
105
+ def initialize(endpoint:, open_timeout: 2, read_timeout: 5, write_timeout: 5, deadline: 15,
106
+ keep_alive: false, keep_alive_timeout: 2, max_idle: 4)
107
+ @endpoint = self.class.endpoint(endpoint)
108
+ @uri = URI(@endpoint)
109
+ @open_timeout, @read_timeout, @write_timeout = [open_timeout, read_timeout, write_timeout].map { |value| seconds(value, 30) }
110
+ @deadline = seconds(deadline, 60)
111
+ raise ConfigurationError, "keep_alive must be true or false" unless [true, false].include?(keep_alive)
112
+ raise ConfigurationError, "max_idle must be an integer from 1 to 64" unless max_idle.is_a?(Integer) && (1..64).cover?(max_idle)
113
+ @keep_alive, @keep_alive_timeout, @max_idle = keep_alive, seconds(keep_alive_timeout, 30), max_idle
114
+ @lock, @idle, @pid = Mutex.new, [], Process.pid
115
+ end
116
+
117
+ def keep_alive? = @keep_alive
118
+
119
+ def call(method, path, headers:, body: nil)
120
+ request = REQUESTS.fetch(method).new(path, headers)
121
+ request.body = body if body
122
+ connection = checkout(clock + @deadline)
123
+ response = exchange(connection, request)
124
+ checkin(connection)
125
+ connection = nil
126
+ response
127
+ rescue *FAILURES
128
+ raise Unavailable, "authentication service unavailable", cause: nil
129
+ ensure
130
+ discard(connection)
131
+ end
132
+
133
+ def close
134
+ idle = @lock.synchronize { @pid == Process.pid ? @idle.slice!(0..) : [] }
135
+ idle.each { |connection| discard(connection) }
136
+ nil
137
+ end
138
+
139
+ private
140
+
141
+ def checkout(expires)
142
+ connection = reusable || build
143
+ connection.expires = expires
144
+ connection.open_timeout = connection.budget(@open_timeout)
145
+ connection.started? ? connection : connection.start
146
+ end
147
+
148
+ def reusable
149
+ return unless @keep_alive
150
+ @lock.synchronize do
151
+ @idle, @pid = [], Process.pid unless @pid == Process.pid
152
+ @idle.pop
153
+ end
154
+ end
155
+
156
+ def checkin(connection)
157
+ kept = @keep_alive && @lock.synchronize do
158
+ @pid == Process.pid && @idle.size < @max_idle && @idle.push(connection)
159
+ end
160
+ discard(connection) unless kept
161
+ end
162
+
163
+ def build
164
+ http = Connection.new(@uri.hostname, @uri.port, nil)
165
+ http.read_timeout, http.write_timeout = @read_timeout, @write_timeout
166
+ http.keep_alive_timeout = @keep_alive_timeout
167
+ http.max_retries = 0
168
+ http.use_ssl = @uri.scheme == "https"
169
+ if http.use_ssl?
170
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
171
+ http.verify_hostname = true
172
+ http.min_version = OpenSSL::SSL::TLS1_2_VERSION
173
+ end
174
+ http
175
+ end
176
+
177
+ def exchange(connection, request)
178
+ connection.write_timeout = connection.budget(@write_timeout)
179
+ result = nil
180
+ connection.request(request) do |response|
181
+ bytes = +""
182
+ response.read_body do |chunk|
183
+ raise ProtocolError, "authentication response too large" if bytes.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
184
+ bytes << chunk
185
+ end
186
+ result = TransportResponse.new(status: response.code, headers: response.to_hash, body: bytes)
187
+ end
188
+ result
189
+ end
190
+
191
+ def discard(connection)
192
+ connection.finish if connection&.started?
193
+ rescue *FAILURES
194
+ nil
195
+ end
196
+
197
+ def seconds(value, limit)
198
+ number = Float(value)
199
+ raise ArgumentError unless number.finite? && number.positive? && number <= limit
200
+ number
201
+ rescue ArgumentError, TypeError
202
+ raise ConfigurationError, "timeouts must be positive and at most #{limit} seconds", cause: nil
203
+ end
204
+
205
+ def clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
206
+ end
207
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strongroom
4
+ module Rails
5
+ class Account
6
+ attr_reader :client, :table, :model, :register, :cookie_name
7
+
8
+ def initialize(client:, table:, model:, resolve:, eligible:, register: false)
9
+ raise ConfigurationError, "client must be a Strongroom::Client" unless client.is_a?(Client)
10
+ unless table.is_a?(String) && table.match?(/\A[a-z_][a-z0-9_]{0,62}\z/)
11
+ raise ConfigurationError, "table must be a lowercase SQL identifier of at most 63 characters"
12
+ end
13
+ raise ConfigurationError, "table #{table} is a PostgreSQL system name (pg_*)" if table.start_with?("pg_")
14
+ unless model.is_a?(Class) && model.respond_to?(:table_name)
15
+ raise ConfigurationError, "model must be a class responding to table_name, not #{model.inspect}"
16
+ end
17
+ unless model.table_name == table
18
+ raise ConfigurationError, "#{model}.table_name is #{model.table_name.inspect}, not #{table.inspect}"
19
+ end
20
+ raise ConfigurationError, "resolve must be callable (subject -> record or nil)" unless resolve.respond_to?(:call)
21
+ raise ConfigurationError, "eligible must be callable (record -> true)" unless eligible.respond_to?(:call)
22
+ raise ConfigurationError, "register must be true or false" unless [true, false].include?(register)
23
+ @client, @model, @table = client, model, table.dup.freeze
24
+ @resolver, @eligible, @register = resolve, eligible, register
25
+ @cookie_name = "strongroom_#{client.namespace}".freeze
26
+ freeze
27
+ end
28
+
29
+ def resolve(session)
30
+ subject = client.subject(session)
31
+ record = @resolver.call(subject)
32
+ unless record.is_a?(model) && record.class.table_name == table && @eligible.call(record).equal?(true)
33
+ raise Unauthorized, "authentication failed"
34
+ end
35
+ record
36
+ rescue Error
37
+ raise
38
+ rescue StandardError
39
+ raise Unavailable, "local principal lookup unavailable", cause: nil
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+ require "action_controller/metal/request_forgery_protection"
5
+
6
+ module Strongroom
7
+ module Rails
8
+ module Authentication
9
+ extend ActiveSupport::Concern
10
+
11
+ INTENT_HEADER = "HTTP_X_STRONGROOM_REQUEST"
12
+
13
+ def self.origin(value)
14
+ NetHTTPTransport.endpoint(value)
15
+ rescue ConfigurationError
16
+ raise ConfigurationError, "trusted origins must be canonical HTTPS origins (HTTP only on loopback)", cause: nil
17
+ end
18
+
19
+ included do
20
+ prepend_before_action :strongroom_no_store!
21
+ helper_method :strongroom_current_account if respond_to?(:helper_method)
22
+ class_attribute :strongroom_trusted_origins, instance_accessor: false, default: [].freeze
23
+ end
24
+
25
+ class_methods do
26
+ def strongroom_trust_origins(*origins)
27
+ self.strongroom_trusted_origins = origins.flatten.map { |origin| Authentication.origin(origin) }.uniq.freeze
28
+ end
29
+ end
30
+
31
+ def strongroom_login(namespace:, identity:, password:, identity_kind: "email")
32
+ strongroom_verify_mutation!(:post)
33
+ account = strongroom_registry.fetch(namespace)
34
+ issued = account.client.login(identity: identity, password: password, identity_kind: identity_kind)
35
+ strongroom_publish(account, issued)
36
+ ensure
37
+ strongroom_resolver.forget(namespace)
38
+ end
39
+
40
+ def strongroom_current_account(namespace:)
41
+ strongroom_resolver.account(namespace)
42
+ end
43
+
44
+ def strongroom_rotate(namespace:)
45
+ strongroom_verify_mutation!(:post)
46
+ account = strongroom_registry.fetch(namespace)
47
+ token = strongroom_resolver.credential(namespace) or raise Unauthorized, "authentication failed"
48
+ strongroom_publish(account, account.client.rotate(token: token))
49
+ ensure
50
+ strongroom_resolver.forget(namespace)
51
+ end
52
+
53
+ def strongroom_logout(namespace:)
54
+ strongroom_verify_mutation!(:delete)
55
+ account = strongroom_registry.fetch(namespace)
56
+ resolver = strongroom_resolver
57
+ presented, cookie = resolver.credential(namespace), resolver.cookie_credential(namespace)
58
+ if presented.nil? && cookie.nil? && !request.get_header("HTTP_AUTHORIZATION").nil?
59
+ raise Unauthorized, "no revocable session credential"
60
+ end
61
+ [presented, cookie].compact.uniq(&:reveal).each { |token| account.client.logout(token: token) }
62
+ request.cookie_jar.delete(account.cookie_name, path: "/", secure: true, httponly: true, same_site: :lax)
63
+ nil
64
+ ensure
65
+ strongroom_resolver.forget(namespace)
66
+ end
67
+
68
+ private
69
+
70
+ def strongroom_no_store!
71
+ response.set_header("Cache-Control", "no-store")
72
+ response.set_header("Vary", [response.get_header("Vary"), "Cookie", "Authorization"].compact.join(", "))
73
+ end
74
+
75
+ def strongroom_registry = Registry.current
76
+ def strongroom_resolver = Resolver.new(request)
77
+
78
+ def strongroom_verify_mutation!(verb)
79
+ unless request.request_method == verb.to_s.upcase && strongroom_mutation_intended?
80
+ raise ::ActionController::InvalidAuthenticityToken, "verified mutation intent required"
81
+ end
82
+ end
83
+
84
+ def strongroom_mutation_intended?
85
+ if respond_to?(:verified_request?, true)
86
+ protect_against_forgery? && verified_request?
87
+ else
88
+ strongroom_declared_intent?
89
+ end
90
+ end
91
+
92
+ def strongroom_declared_intent?
93
+ return false unless request.get_header(INTENT_HEADER) == "1"
94
+ origin = request.get_header("HTTP_ORIGIN")
95
+ return request.get_header("HTTP_SEC_FETCH_SITE").nil? if origin.nil?
96
+ origin == request.base_url || self.class.strongroom_trusted_origins.include?(origin)
97
+ end
98
+
99
+ def strongroom_publish(account, issued)
100
+ begin
101
+ record = account.resolve(issued.session)
102
+ rescue Error => error
103
+ account.client.discard(token: issued.token)
104
+ raise error, cause: nil
105
+ end
106
+ request.cookie_jar.encrypted[account.cookie_name] = {
107
+ value: issued.token.reveal, expires: issued.session.expires_at,
108
+ secure: true, httponly: true, same_site: :lax, path: "/"
109
+ }
110
+ record
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strongroom
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ config.strongroom = ActiveSupport::OrderedOptions.new
7
+ config.strongroom.verify_on_boot = true
8
+
9
+ config.action_dispatch.rescue_responses.merge!(
10
+ "Strongroom::InvalidRequest" => :bad_request,
11
+ "Strongroom::Unauthorized" => :unauthorized,
12
+ "Strongroom::RateLimited" => :too_many_requests,
13
+ "Strongroom::Busy" => :service_unavailable,
14
+ "Strongroom::ProtocolError" => :service_unavailable,
15
+ "Strongroom::Unavailable" => :service_unavailable
16
+ )
17
+
18
+ initializer "strongroom.filter_credentials" do |app|
19
+ app.config.filter_parameters |= %i[password password_confirmation identity authorization cookie strongroom_token]
20
+ end
21
+
22
+ config.after_initialize do |app|
23
+ require_relative "warden_strategy" if defined?(::Warden::Strategies)
24
+ if app.config.strongroom.verify_on_boot
25
+ Railtie.verify!(app)
26
+ app.reloader.to_prepare { Railtie.verify!(app) }
27
+ end
28
+ end
29
+
30
+ def self.verify!(app)
31
+ Registry.current(app)
32
+ unless app.config.middleware.include?(ActionDispatch::Cookies)
33
+ raise ConfigurationError, "ActionDispatch::Cookies middleware is required for Strongroom session cookies"
34
+ end
35
+ nil
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strongroom
4
+ module Rails
5
+ class Registry
6
+ def self.current(app = ::Rails.application)
7
+ raise ConfigurationError, "no Rails application is defined" unless app
8
+ registry = app.config.x.strongroom_accounts
9
+ return registry if registry.is_a?(self)
10
+ if registry.nil? || (registry.is_a?(ActiveSupport::OrderedOptions) && registry.empty?)
11
+ raise ConfigurationError, "config.x.strongroom_accounts is not set: assign a Strongroom::Rails::Registry in a to_prepare block"
12
+ end
13
+ raise ConfigurationError, "config.x.strongroom_accounts must be a Strongroom::Rails::Registry, not #{registry.class}"
14
+ end
15
+
16
+ def initialize(*accounts, warden_scopes: {})
17
+ raise ConfigurationError, "configure one to sixteen accounts, got #{accounts.size}" unless (1..16).cover?(accounts.size)
18
+ unless accounts.all?(Account)
19
+ stray = accounts.find { |account| !account.is_a?(Account) }
20
+ raise ConfigurationError, "registry entries must be Strongroom::Rails::Account, not #{stray.class}"
21
+ end
22
+ @accounts = {}
23
+ accounts.each do |account|
24
+ client = account.client
25
+ raise ConfigurationError, "namespace #{client.namespace} is configured twice" if @accounts.key?(client.namespace)
26
+ @accounts.each_value do |other|
27
+ if other.table == account.table
28
+ raise ConfigurationError, "namespaces #{other.client.namespace} and #{client.namespace} share table #{account.table}"
29
+ end
30
+ if (other.client.issuer == client.issuer || other.client.endpoint == client.endpoint) &&
31
+ (other.client.org_id == client.org_id || other.client.app_id == client.app_id)
32
+ raise ConfigurationError,
33
+ "namespaces #{other.client.namespace} and #{client.namespace} share an org or app on the same issuer or endpoint"
34
+ end
35
+ end
36
+ @accounts[client.namespace] = account
37
+ end
38
+ @accounts.freeze
39
+ @scopes = scopes(warden_scopes)
40
+ freeze
41
+ end
42
+
43
+ def fetch(namespace)
44
+ @accounts.fetch(namespace.to_s) { raise ConfigurationError, "unknown account namespace" }
45
+ end
46
+
47
+ def namespaces = @accounts.keys
48
+
49
+ def for_scope(scope)
50
+ @scopes.fetch(scope.to_s) { fetch(scope) }
51
+ end
52
+
53
+ private
54
+
55
+ def scopes(mapping)
56
+ raise ConfigurationError, "warden_scopes must map Warden scopes to namespaces" unless mapping.is_a?(Hash)
57
+ mapping.to_h do |scope, namespace|
58
+ unless (scope.is_a?(Symbol) || scope.is_a?(String)) && @accounts.key?(namespace.to_s)
59
+ raise ConfigurationError, "Warden scope #{scope.inspect} must map to a configured namespace"
60
+ end
61
+ [scope.to_s.freeze, @accounts.fetch(namespace.to_s)]
62
+ end.freeze
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch"
4
+ require "action_dispatch/middleware/cookies"
5
+
6
+ module Strongroom
7
+ module Rails
8
+ class Resolver
9
+ MEMO = "strongroom.rails.resolved"
10
+ attr_reader :request
11
+
12
+ def initialize(request, registry: nil)
13
+ @request = case request
14
+ when ActionDispatch::Request then request
15
+ when Hash then ActionDispatch::Request.new(request)
16
+ else ActionDispatch::Request.new(request.env)
17
+ end
18
+ @registry = registry
19
+ end
20
+
21
+ def account(namespace)
22
+ remember(:account, namespace) do
23
+ current = session(namespace)
24
+ current && registry.fetch(namespace).resolve(current)
25
+ rescue Unauthorized
26
+ nil
27
+ end
28
+ end
29
+
30
+ def subject(namespace)
31
+ current = session(namespace) or return nil
32
+ registry.fetch(namespace).client.subject(current)
33
+ rescue Unauthorized
34
+ nil
35
+ end
36
+
37
+ def session(namespace)
38
+ remember(:session, namespace) do
39
+ token = credential(namespace)
40
+ token && registry.fetch(namespace).client.session(token: token)
41
+ rescue Unauthorized
42
+ nil
43
+ end
44
+ end
45
+
46
+ def forget(namespace)
47
+ name = namespace.to_s
48
+ memo.delete_if { |(_, key), _| key == name }
49
+ nil
50
+ end
51
+
52
+ def credential(namespace)
53
+ authorization = request.get_header("HTTP_AUTHORIZATION")
54
+ return Token.from_authorization(authorization) unless authorization.nil?
55
+ cookie_credential(namespace)
56
+ end
57
+
58
+ def cookie_credential(namespace)
59
+ value = request.cookie_jar.encrypted[registry.fetch(namespace).cookie_name]
60
+ Token.new(value) unless value.nil?
61
+ rescue Unauthorized
62
+ nil
63
+ end
64
+
65
+ private
66
+
67
+ def registry = @registry ||= Registry.current
68
+ def memo = request.get_header(MEMO) || request.set_header(MEMO, {})
69
+
70
+ def remember(kind, namespace)
71
+ key = [kind, registry.fetch(namespace).client.namespace]
72
+ cache = memo
73
+ return cache[key] if cache.key?(key)
74
+ cache[key] = yield
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strongroom
4
+ module Rails
5
+ VERSION = "0.1.0" # x-release-please-version
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "warden"
4
+
5
+ module Strongroom
6
+ module Rails
7
+ class WardenStrategy < ::Warden::Strategies::Base
8
+ def valid?
9
+ !resolver.credential(namespace).nil?
10
+ end
11
+
12
+ def authenticate!
13
+ record = resolver.account(namespace)
14
+ record ? success!(record) : fail!("authentication failed")
15
+ end
16
+
17
+ def store? = false
18
+
19
+ private
20
+
21
+ def namespace = @namespace ||= Registry.current.for_scope(scope).client.namespace
22
+ def resolver = @resolver ||= Resolver.new(env)
23
+ end
24
+ end
25
+ end
26
+
27
+ ::Warden::Strategies.add(:strongroom, Strongroom::Rails::WardenStrategy) unless ::Warden::Strategies[:strongroom]
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strongroom"
4
+ require "rails"
5
+ require "action_controller"
6
+ require_relative "rails/account"
7
+ require_relative "rails/registry"
8
+ require_relative "rails/resolver"
9
+ require_relative "rails/authentication"
10
+ require_relative "rails/railtie"
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Strongroom
6
+ Session = Data.define(:session_id, :user_id, :org_id, :app_id, :expires_at)
7
+ Subject = Data.define(:issuer, :namespace, :org_id, :app_id, :user_id)
8
+
9
+ class IssuedSession
10
+ attr_reader :session, :token
11
+
12
+ def initialize(session:, token:)
13
+ @session, @token = session, token
14
+ freeze
15
+ end
16
+
17
+ def inspect = "#<Strongroom::IssuedSession session_id=#{session.session_id} token=[FILTERED]>"
18
+ def pretty_print(printer) = printer.text(inspect)
19
+
20
+ def as_json(*)
21
+ raise TypeError, "issued credentials must not be serialized"
22
+ end
23
+ end
24
+ end