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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/LICENSE +202 -28
- data/README.md +2036 -7
- data/lib/triauth/api/attestation.rb +419 -0
- data/lib/triauth/api/authentication.rb +651 -0
- data/lib/triauth/api/identity.rb +245 -0
- data/lib/triauth/api/signing.rb +740 -0
- data/lib/triauth/api/validation.rb +162 -0
- data/lib/triauth/api.rb +33 -0
- data/lib/triauth/authentication_endpoint.rb +196 -0
- data/lib/triauth/challenge.rb +115 -0
- data/lib/triauth/challenge_response_flow.rb +344 -0
- data/lib/triauth/config.rb +93 -0
- data/lib/triauth/error.rb +95 -0
- data/lib/triauth/helpers.rb +1222 -0
- data/lib/triauth/identity.rb +301 -0
- data/lib/triauth/identity_domain.rb +212 -0
- data/lib/triauth/identity_includes.rb +249 -0
- data/lib/triauth/identity_keys.rb +309 -0
- data/lib/triauth/logger.rb +156 -0
- data/lib/triauth/multi_signature.rb +177 -0
- data/lib/triauth/protocol.rb +74 -0
- data/lib/triauth/request_cipher.rb +121 -0
- data/lib/triauth/resolvers/base.rb +147 -0
- data/lib/triauth/resolvers/caching_resolver.rb +202 -0
- data/lib/triauth/resolvers/cloudflare.rb +40 -0
- data/lib/triauth/resolvers/dns_json.rb +391 -0
- data/lib/triauth/resolvers/dns_sb.rb +40 -0
- data/lib/triauth/resolvers/google.rb +39 -0
- data/lib/triauth/resolvers/multi_resolver.rb +202 -0
- data/lib/triauth/resolvers/system_dns.rb +259 -0
- data/lib/triauth/resolvers.rb +33 -0
- data/lib/triauth/response.rb +142 -0
- data/lib/triauth/response_cipher.rb +341 -0
- data/lib/triauth/signature.rb +405 -0
- data/lib/triauth/validator.rb +549 -0
- data/lib/triauth/verifiers/base.rb +147 -0
- data/lib/triauth/verifiers/ecdsa.rb +87 -0
- data/lib/triauth/verifiers/ed25519.rb +89 -0
- data/lib/triauth/verifiers/registry.rb +52 -0
- data/lib/triauth/verifiers/web_authn.rb +253 -0
- data/lib/triauth/verifiers.rb +37 -0
- data/lib/triauth/version.rb +23 -0
- data/lib/triauth.rb +104 -7
- 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
|
data/lib/triauth/api.rb
ADDED
|
@@ -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
|