triauth 0.0.1 → 1.0.0.beta.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE +202 -28
  4. data/README.md +2036 -7
  5. data/lib/triauth/api/attestation.rb +419 -0
  6. data/lib/triauth/api/authentication.rb +651 -0
  7. data/lib/triauth/api/identity.rb +245 -0
  8. data/lib/triauth/api/signing.rb +740 -0
  9. data/lib/triauth/api/validation.rb +162 -0
  10. data/lib/triauth/api.rb +33 -0
  11. data/lib/triauth/authentication_endpoint.rb +196 -0
  12. data/lib/triauth/challenge.rb +115 -0
  13. data/lib/triauth/challenge_response_flow.rb +344 -0
  14. data/lib/triauth/config.rb +93 -0
  15. data/lib/triauth/error.rb +95 -0
  16. data/lib/triauth/helpers.rb +1222 -0
  17. data/lib/triauth/identity.rb +301 -0
  18. data/lib/triauth/identity_domain.rb +212 -0
  19. data/lib/triauth/identity_includes.rb +249 -0
  20. data/lib/triauth/identity_keys.rb +309 -0
  21. data/lib/triauth/logger.rb +156 -0
  22. data/lib/triauth/multi_signature.rb +177 -0
  23. data/lib/triauth/protocol.rb +74 -0
  24. data/lib/triauth/request_cipher.rb +121 -0
  25. data/lib/triauth/resolvers/base.rb +147 -0
  26. data/lib/triauth/resolvers/caching_resolver.rb +202 -0
  27. data/lib/triauth/resolvers/cloudflare.rb +40 -0
  28. data/lib/triauth/resolvers/dns_json.rb +391 -0
  29. data/lib/triauth/resolvers/dns_sb.rb +40 -0
  30. data/lib/triauth/resolvers/google.rb +39 -0
  31. data/lib/triauth/resolvers/multi_resolver.rb +202 -0
  32. data/lib/triauth/resolvers/system_dns.rb +259 -0
  33. data/lib/triauth/resolvers.rb +33 -0
  34. data/lib/triauth/response.rb +142 -0
  35. data/lib/triauth/response_cipher.rb +341 -0
  36. data/lib/triauth/signature.rb +405 -0
  37. data/lib/triauth/validator.rb +549 -0
  38. data/lib/triauth/verifiers/base.rb +147 -0
  39. data/lib/triauth/verifiers/ecdsa.rb +87 -0
  40. data/lib/triauth/verifiers/ed25519.rb +89 -0
  41. data/lib/triauth/verifiers/registry.rb +52 -0
  42. data/lib/triauth/verifiers/web_authn.rb +253 -0
  43. data/lib/triauth/verifiers.rb +37 -0
  44. data/lib/triauth/version.rb +23 -0
  45. data/lib/triauth.rb +104 -7
  46. metadata +55 -13
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 The Triauth Authors (https://www.triauth.org/)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+
20
+ require_relative '../config'
21
+ require_relative '../helpers'
22
+ require_relative '../validator'
23
+
24
+ module Triauth
25
+ module Api
26
+ # Methods related to validation of various objects related to the triauth protocol
27
+ module Validation
28
+ # The validate method can be used to perform a basic format validation on the given objects.
29
+ # It is fast, synchronous, and does not perform any DNS requests.
30
+ #
31
+ # Currently, it supports validation of personal identifiers (`identifier`), device names (`deviceName`),
32
+ # group names (`groupName`, the bare name as published in a `groups` record, e.g., `admins`),
33
+ # lookup codes (`lookupCode`, the 16-character code of an identity under a `mode=private` domain),
34
+ # and device tags (`deviceTag`, as reported in results and passed to `Triauth.check`).
35
+ # At least one of the supported objects must be provided, and unrecognized (e.g., misspelled)
36
+ # keys are rejected, so that nothing silently passes as valid without being validated.
37
+ #
38
+ # Synopsis:
39
+ #
40
+ # ```ruby
41
+ # validation_result = Triauth.validate({ identifier?, deviceName?, groupName?, lookupCode?, deviceTag? })
42
+ # # returns {valid: true, errors: []} if all the objects passed validation
43
+ # # returns {valid: false, errors: [{code: ..., reason: ..., message: ...}, ...]} if at least one the objects failed validation
44
+ # # returns {valid: false, errors: [{code: 'invalid_arguments', ...}]} when objects is empty or not a Hash, or contains an unrecognized key
45
+ # # returns {valid: false, errors: [{code: 'invalid_config', ...}]} when config contains an unrecognized key
46
+ #
47
+ # ```
48
+ #
49
+ # Example:
50
+ #
51
+ # ```ruby
52
+ # Triauth.validate({ identifier: 'john@triauthdemo.org' })
53
+ # # {valid: true, errors: []}
54
+ #
55
+ # Triauth.validate({ identifier: 'john' })
56
+ # # {valid: false, errors: [{code: 'invalid_identifier', reason: 'invalid_format', message: 'Identifier must include one @ sign'}]}
57
+ #
58
+ # Triauth.validate({ groupName: 'admins@example.com' })
59
+ # # {valid: false, errors: [{code: 'invalid_group_name', reason: 'invalid_characters', message: 'Group name must include only letters, numbers, and hyphens'}]}
60
+ #
61
+ # Triauth.validate({ lookupCode: 'k7qj3fb9m2wzx0c4' })
62
+ # # {valid: false, errors: [{code: 'invalid_lookup_code', reason: 'invalid', message: 'Invalid lookup code'}]}
63
+ #
64
+ # Triauth.validate({ deviceTag: '3bi5oE_jXyjGY0WY5n6YqOuzRxfo40fuqDmLGaA5bKQ~K7QJ3FB9M2WZX0C4' })
65
+ # # {valid: true, errors: []}
66
+ # ```
67
+ #
68
+ # > [!NOTE]
69
+ # > `{valid: true}` confirms the *format* only - it is not input sanitization.
70
+ # > Callers must still encode or escape the value for whatever sink it flows into (SQL, shell, file paths, HTML, log lines).
71
+ #
72
+ # @param objects [Hash] one or more objects to validate. Defaults to nil (a missing argument is rejected with `invalid_arguments`).
73
+ # @option objects [String] :identifier an identifier to validate
74
+ # @option objects [String] :deviceName a deviceName to validate
75
+ # @option objects [String] :groupName a bare group name to validate (e.g., `admins`)
76
+ # @option objects [String] :lookupCode a lookup code to validate (e.g., `K7QJ3FB9M2WZX0C4`)
77
+ # @option objects [String] :deviceTag a device tag to validate, plain or composite (e.g., `3bi5oE_jXyjGY0WY5n6YqOuzRxfo40fuqDmLGaA5bKQ`)
78
+ # @param config [Hash] optional overrides for the global Triauth.config settings; unrecognized keys are rejected (`invalid_config`). Defaults to `{}`.
79
+ #
80
+ # @return [Hash] `{valid: Boolean, errors: Array<{code: String, reason?: String, message: String}>}`
81
+ # A Hash with valid property containing the validation result,
82
+ # and the errors property containing an array of errors (if any),
83
+ # in the `{code, reason?, message}` format.
84
+ def self.validate(objects = nil, config = {})
85
+ # `objects` must be a plain object with at least one supported entry,
86
+ # so that e.g. validate(null) or validate({}) cannot pass as valid.
87
+ # (`!objects || typeof objects !== 'object' || Array.isArray(objects)` is "not a Hash": nil, false, strings, numbers, true and Arrays all take this branch, as in JS)
88
+ if !objects.is_a?(Hash) || objects.empty?
89
+ return {
90
+ valid: false,
91
+ errors: [{ code: 'invalid_arguments', message: 'Invalid or missing function arguments' }]
92
+ }
93
+ end
94
+
95
+ # Ensure only recognized objects with defined values are present to help avoid misspellings.
96
+ if !Helpers.has_only_known_properties(objects, %i[identifier deviceName groupName lookupCode deviceTag])
97
+ return {
98
+ valid: false,
99
+ errors: [{ code: 'invalid_arguments', message: 'Unrecognized function argument' }]
100
+ }
101
+ end
102
+
103
+ # Reject config overrides carrying unknown keys
104
+ if !Helpers.has_only_known_properties(Helpers.merge_config(config), Triauth::KNOWN_CONFIG_KEYS)
105
+ return {
106
+ valid: false,
107
+ errors: [{ code: 'invalid_config', message: 'Unrecognized configuration key' }]
108
+ }
109
+ end
110
+
111
+ retval = {
112
+ valid: true,
113
+ errors: []
114
+ }
115
+
116
+ keys = objects.keys
117
+
118
+ if keys.include?(:identifier)
119
+ vr = Validator.validate_identifier(objects[:identifier])
120
+ if !vr[:valid]
121
+ retval[:valid] = false
122
+ retval[:errors] = retval[:errors] + vr[:errors] # `concat` returns a new array (never `Array#concat`, which mutates)
123
+ end
124
+ end
125
+
126
+ if keys.include?(:deviceName)
127
+ vr = Validator.validate_device_name(objects[:deviceName])
128
+ if !vr[:valid]
129
+ retval[:valid] = false
130
+ retval[:errors] = retval[:errors] + vr[:errors]
131
+ end
132
+ end
133
+
134
+ if keys.include?(:groupName)
135
+ vr = Validator.validate_group_name(objects[:groupName])
136
+ if !vr[:valid]
137
+ retval[:valid] = false
138
+ retval[:errors] = retval[:errors] + vr[:errors]
139
+ end
140
+ end
141
+
142
+ if keys.include?(:lookupCode)
143
+ vr = Validator.validate_lookup_code(objects[:lookupCode])
144
+ if !vr[:valid]
145
+ retval[:valid] = false
146
+ retval[:errors] = retval[:errors] + vr[:errors]
147
+ end
148
+ end
149
+
150
+ if keys.include?(:deviceTag)
151
+ vr = Validator.validate_device_tag(objects[:deviceTag])
152
+ if !vr[:valid]
153
+ retval[:valid] = false
154
+ retval[:errors] = retval[:errors] + vr[:errors]
155
+ end
156
+ end
157
+
158
+ return retval
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 The Triauth Authors (https://www.triauth.org/)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+
20
+ module Triauth
21
+ # The API namespace holds methods that are a part of the official triauth-ruby API.
22
+ #
23
+ # Every API method is also aliased at the top level, so `Triauth::Api::Authentication.authenticate` and
24
+ # `Triauth.authenticate` are the same function.
25
+ module Api
26
+ end
27
+ end
28
+
29
+ require_relative 'api/authentication'
30
+ require_relative 'api/attestation'
31
+ require_relative 'api/identity'
32
+ require_relative 'api/signing'
33
+ require_relative 'api/validation'
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 The Triauth Authors (https://www.triauth.org/)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+
20
+ require_relative 'helpers'
21
+ require_relative 'protocol'
22
+
23
+ module Triauth
24
+ # Holds information about authentication endpoint for a given domain.
25
+ #
26
+ # Authentication endpoint is the URL under which Triauth Authenticator application, for the given identifier and its domain,
27
+ # is available and can be communicated with.
28
+ #
29
+ # Objects of this class start in unresolved state, and need to be resolved with a call to resolve() that queries the DNS.
30
+ class AuthenticationEndpoint
31
+ # The option keys the `triauth` record defines. The endpoint keeps these and the `x-` extensions and ignores
32
+ # every other option of the record, so a record that spells a future option still configures a verifier that
33
+ # predates it, and no other published name reaches a result.
34
+ ENDPOINT_OPTIONS = %i[mode include].freeze
35
+ private_constant :ENDPOINT_OPTIONS
36
+
37
+ # @return [String] The domain name for which authentication endpoint shall be resolved.
38
+ attr_reader :domain_name
39
+ # @return [Hash{Symbol => Object}] The effective settings: the global `Triauth.config` with the constructor's overrides merged over it.
40
+ attr_reader :config
41
+ # The endpoint's lowercase host — the value of the domain's `triauth` DNS record.
42
+ # `nil` until resolved.
43
+ # @return [String, nil]
44
+ attr_reader :host
45
+ # The `mode` and `include` options as read from the `triauth` DNS record, with the defaults filled in when
46
+ # absent, and its `x-` extension options as published. Every other option of the record is ignored.
47
+ # Empty until resolved.
48
+ # @return [Hash{Symbol => Object}]
49
+ attr_reader :options
50
+ # Whether the DNS record defining this authentication endpoint was protected by DNSSEC.
51
+ # `true` only when the resolver reported a validated (AD) response for the matched `triauth` record;
52
+ # `false` when it was not validated or the resolver did not report DNSSEC status;
53
+ # `nil` until resolved.
54
+ # @return [Boolean, nil]
55
+ attr_reader :secure
56
+ # Expiry of the resolved `triauth` record: the resolution time plus its TTL, in milliseconds;
57
+ # `nil` when the record carried no TTL. Every expiry reported for an identity is bounded by it.
58
+ # @return [Integer, nil]
59
+ attr_reader :expires
60
+ # @return [Boolean, nil] Whether the authentication endpoint resolved successfully; `nil` until resolve() has run.
61
+ attr_reader :resolved
62
+
63
+ # Constructs a new instance of AuthenticationEndpoint basing on the domain name extracted form the identifier.
64
+ #
65
+ # Objects of this class start in an un-resolved state, and a call to the resolve() that
66
+ # performs the DNS resolution needs to be made before most of the other methods can be used.
67
+ #
68
+ # @param domain_name [String] The domain name for which authentication endpoint shall be resolved.
69
+ # @param config [Hash] Optional overrides for the global Triauth.config settings. Defaults to `{}`.
70
+ def initialize(domain_name, config = {})
71
+ # Memoizes the resolution, so that concurrent and repeated `resolve()` calls share a single
72
+ # DNS round trip. A rejection is memoized too, and is replayed to every later caller.
73
+ # The memoized outcome of the first resolution: `[true, result]` or `[false, error]`, guarded by the mutex
74
+ # (the port of the `#resolvePromise` promise).
75
+ @resolve_mutex = Mutex.new
76
+ @resolve_promise = nil
77
+
78
+ @domain_name = domain_name
79
+ @config = Helpers.merge_config(config)
80
+
81
+ @host = nil
82
+
83
+ @options = {}
84
+
85
+ @secure = nil
86
+
87
+ @expires = nil
88
+
89
+ @resolved = nil
90
+ end
91
+
92
+ # Keeps the options the `triauth` record defines, `mode` and `include`, and its `x-` extensions, and
93
+ # leaves every other option of the record out.
94
+ #
95
+ # @param options [Hash{Symbol => Object}] The options of the record, as parsed.
96
+ #
97
+ # @return [Hash{Symbol => Object}] the kept options, in a fresh Hash (the JS map without a prototype).
98
+ def self.known_options(options)
99
+ known = {}
100
+
101
+ options.each do |key, value|
102
+ if ENDPOINT_OPTIONS.include?(key) || (key[0] == 'x' && key[1] == '-')
103
+ known[key] = value
104
+ end
105
+ end
106
+
107
+ return known
108
+ end
109
+
110
+ # Returns the URL of the authentication endpoint, with the passed action and parameters included.
111
+ #
112
+ # With an action, it is the final URL to which the user's web browser should be redirected in the challenge-response flow.
113
+ # The params are passed inside the URL fragment identifier for privacy reasons.
114
+ #
115
+ # @param action [String, nil] An action verb used to construct the URL - e.g., 'auth', 'sign', etc., or nil for the bare endpoint URL.
116
+ # @param params [Hash] Parameters that should be included in the URL, ignored when action is nil (and then omitted: defaults to nil).
117
+ #
118
+ # @return [String] an url of authentication endpoint with the given action and parameters.
119
+ #
120
+ # @raise [RuntimeError] when the endpoint has not been resolved yet - call `resolve()` first.
121
+ def url_for(action, params = nil)
122
+ if !@resolved
123
+ raise 'Unresolved AuthenticationEndpoint'
124
+ end
125
+
126
+ if action.nil?
127
+ return "https://#{@host}/"
128
+ end
129
+
130
+ # Parameter values are base64url by the time they get here
131
+ pairs = params.map { |key, value| "#{key}=#{value}" }
132
+
133
+ return "https://#{@host}/#{action}.html#?#{pairs.join('&')}"
134
+ end
135
+
136
+ # Resolves the authentication endpoint for the domain by querying the DNS,
137
+ # and sets the `host`, `options`, and `secure` properties accordingly basing on the DNS response.
138
+ #
139
+ # The result is memoized, and subsequent calls to `resolve` do not perform DNS lookups.
140
+ # Rejections are currently memoized too.
141
+ #
142
+ # @return [Boolean] true if the URL of authentication endpoint has been resolved, false otherwise.
143
+ def resolve
144
+ # Memoize the outcome so that multiple and/or concurrent calls return the same status.
145
+ @resolve_mutex.synchronize do
146
+ @resolve_promise ||= begin
147
+ # The body of the memoized resolution is a lambda, so that every JS `return` is a `return` of the lambda.
148
+ resolution = lambda do
149
+ Helpers.log(@config, :debug, "Resolving Triauth.AuthenticationEndpoint for domain #{@domain_name}")
150
+
151
+ domain_config = @config[:resolver].resolve_config(@domain_name, @config)
152
+
153
+ domain_config.each do |entry|
154
+ if entry[:key] == 'triauth' && Helpers.is_domain_name(entry[:value])
155
+ if !@host.nil? && !@host.empty?
156
+ # Multiple triauth records published — refuse to choose
157
+ Helpers.log(@config, :warn, 'Multiple triauth records found; refusing to choose', { domainName: @domain_name })
158
+ @host = nil
159
+ @options = {}
160
+ @secure = nil
161
+ @expires = nil
162
+ break
163
+ end
164
+
165
+ @host = entry[:value].downcase
166
+ @options = AuthenticationEndpoint.known_options(entry[:options])
167
+ @secure = entry[:dnssec] == true
168
+ @expires = Helpers.is_integer(entry[:ttl]) ? Helpers.date_now + (entry[:ttl] * 1000) : nil
169
+
170
+ # Set defaults
171
+ @options[:mode] = Protocol::DEFAULT_MODE if @options[:mode].nil?
172
+ @options[:include] = Protocol::DEFAULT_INCLUDE if @options[:include].nil?
173
+ end
174
+ end
175
+
176
+ Helpers.log(@config, :debug, "Triauth.AuthenticationEndpoint for domain #{@domain_name} is #{@host}")
177
+
178
+ @resolved = !@host.nil? && !@host.empty?
179
+ return [true, @resolved]
180
+ end
181
+ resolution.call
182
+ rescue StandardError => err
183
+ Helpers.log(@config, :debug, err)
184
+
185
+ @resolved = false
186
+ [false, err]
187
+ end
188
+ end
189
+
190
+ ok, value = @resolve_promise
191
+ raise value if !ok
192
+
193
+ return value
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2026 The Triauth Authors (https://www.triauth.org/)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+
19
+
20
+ require 'json'
21
+ require_relative 'helpers'
22
+ require_relative 'identity'
23
+ require_relative 'protocol'
24
+
25
+ module Triauth
26
+ # Represents an authentication request (challenge) that is sent from client application to the Triauth Authenticator.
27
+ class Challenge
28
+ # @return [Triauth::Identity] The Identity the challenge is addressed to.
29
+ attr_reader :identity
30
+ # @return [Hash{Symbol => Object}] The challenge's wire fields - `type`, `identifier`, `nonce`, `iat` and `ver`, plus any per-flow additions.
31
+ attr_reader :data
32
+ # @return [Hash{Symbol => Object}] The effective settings: the global `Triauth.config` with the constructor's overrides merged over it.
33
+ attr_reader :config
34
+ # Populated by Challenge.from_string when re-hydrating a serialized challenge; left nil
35
+ # for freshly built challenges (use to_s to serialize on demand).
36
+ # @return [String, nil]
37
+ attr_accessor :challenge_string
38
+
39
+ # Recreates the Challenge from the given challenge.
40
+ #
41
+ # @param challenge_string [String] a challenge as sent to the authenticator: the part of the challenge stored during
42
+ # stage 1 of the challenge-response flow before its `.` and decryption key.
43
+ # @param config [Hash] optional overrides for the global Triauth.config settings. Defaults to `{}`.
44
+ #
45
+ # @return [Triauth::Challenge]
46
+ # @raise [StandardError] upon problems with deserialization of the challenge or the format of personal identifier encoded within
47
+ def self.from_string(challenge_string, config = {})
48
+ decoded_challenge = Helpers.base64url_to_string(challenge_string)
49
+ challenge_data = Helpers.safe_parse_json(decoded_challenge)
50
+ identity = Identity.new(challenge_data[:identifier], {}, config)
51
+
52
+ retval = Challenge.new(identity, challenge_data, config)
53
+ retval.challenge_string = challenge_string
54
+
55
+ return retval
56
+ end
57
+
58
+ # Builds a new Challenge and populates the required request data fields with the default values,
59
+ # unless a value for them was provided through the data argument.
60
+ #
61
+ # @param type [String] the type of request associated with this challenge, e.g., 'auth', 'sign', etc.
62
+ # @param identity [Triauth::Identity] identity for which the authentication request is being built.
63
+ # @param data [Hash] data to include in the authentication request, may override the defaults
64
+ # @param config [Hash] optional overrides for the global Triauth.config settings. Defaults to `{}`.
65
+ #
66
+ # @return [Triauth::Challenge]
67
+ def self.build(type, identity, data, config = {})
68
+ config = Helpers.merge_config(config)
69
+
70
+ # Populate the data fields that must be present in the triauth authentication request with default values
71
+ data = Helpers.object_assign(
72
+ {},
73
+ data,
74
+ {
75
+ type: type,
76
+ identifier: identity.identifier, # the Identifier the request is addressed to
77
+ nonce: Helpers.random_string(24, config), # cryptographic nonce
78
+ iat: Helpers.date_now, # timestamp of when authentication request was created
79
+ ver: Protocol::VERSION
80
+ }
81
+ )
82
+
83
+ return Challenge.new(identity, data, config)
84
+ end
85
+
86
+ # Constructs a new instance of Challenge for the given identity and challenge data.
87
+ #
88
+ # Use the `build` factory to mint a fresh challenge, or `from_string` to re-hydrate a serialized
89
+ # one; this constructor stores what it is given and validates nothing on its own.
90
+ #
91
+ # @param identity [Triauth::Identity] The Identity the challenge is addressed to.
92
+ # @param data [Hash] The challenge's wire fields - `type`, `identifier`, `nonce`, `iat` and
93
+ # `ver`, plus any per-flow additions such as `msg`, `attest` or `attachments`. Defaults to `{}`.
94
+ # @param config [Hash] optional overrides for the global Triauth.config settings. Defaults to `{}`.
95
+ def initialize(identity, data = {}, config = {})
96
+ @identity = identity
97
+ @data = data
98
+ @config = Helpers.merge_config(config)
99
+
100
+ # Populated by Challenge.from_string when re-hydrating a serialized challenge; left nil
101
+ # for freshly built challenges (use to_s to serialize on demand).
102
+ @challenge_string = nil
103
+ end
104
+
105
+ # Serializes this authentication request into a string that may be used as a challenge.
106
+ #
107
+ # The port of `toString()`.
108
+ #
109
+ # @return [String] a challenge which can be included in the redirect URL
110
+ def to_s
111
+ challenge_json = JSON.generate(@data)
112
+ return Helpers.string_to_base64url(challenge_json)
113
+ end
114
+ end
115
+ end