ohauthcat 0.0.2
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/LICENSE +35 -0
- data/README.md +94 -0
- data/THIRD_PARTY_LICENSES +5 -0
- data/lib/ohauthcat/access_policy.rb +14 -0
- data/lib/ohauthcat/client.rb +333 -0
- data/lib/ohauthcat/errors.rb +98 -0
- data/lib/ohauthcat/generated.rb +2898 -0
- data/lib/ohauthcat/model.rb +129 -0
- data/lib/ohauthcat/models.rb +5127 -0
- data/lib/ohauthcat/passkey_helpers.rb +53 -0
- data/lib/ohauthcat/route.rb +6 -0
- data/lib/ohauthcat/sso_helpers.rb +100 -0
- data/lib/ohauthcat/standby.rb +129 -0
- data/lib/ohauthcat/version.rb +5 -0
- data/lib/ohauthcat/webhooks.rb +97 -0
- data/lib/ohauthcat.rb +20 -0
- metadata +58 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OhAuthCat
|
|
4
|
+
# Headless end-user passkey helpers: session sign-in and passkey rename.
|
|
5
|
+
class PasskeyHelpers
|
|
6
|
+
# The session a passkey sign-in created: the token set issued by the login host,
|
|
7
|
+
# audience the client ID, as /user_management/token would return it.
|
|
8
|
+
PasskeySession = Struct.new(:authentication, :session_id, :access_token, :refresh_token, :id_token,
|
|
9
|
+
:token_type, :expires_in, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
def initialize(client)
|
|
12
|
+
@client = client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Trims value and raises request_invalid when it is blank, longer than maximum
|
|
16
|
+
# characters or contains control characters.
|
|
17
|
+
def self.bounded(value, maximum)
|
|
18
|
+
raise Error.client("request_invalid") unless value.is_a?(String)
|
|
19
|
+
|
|
20
|
+
text = value.strip
|
|
21
|
+
raise Error.client("request_invalid") if text.empty? || text.length > maximum || text.match?(/[\x00-\x1f\x7f]/)
|
|
22
|
+
|
|
23
|
+
text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Verifies a WebAuthn assertion (the decoded JSON credential) and creates a session for
|
|
27
|
+
# client_id (the configured publishable key by default). When the environment requires
|
|
28
|
+
# MFA and the assertion was not user-verified, the API answers mfa_required instead.
|
|
29
|
+
def authenticate_with_session(credential, client_id: nil)
|
|
30
|
+
raise Error.client("request_invalid") unless credential.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
audience = client_id.nil? ? @client.client_id : self.class.bounded(client_id, 256)
|
|
33
|
+
raise Error.client("request_invalid") if audience.nil?
|
|
34
|
+
|
|
35
|
+
body = { "credential" => credential, "session" => true, "client_id" => audience }
|
|
36
|
+
result = Models::EndUserPasskeyAuthentication.from_json(@client.request("authenticateEndUserPasskey", [], nil, body))
|
|
37
|
+
tokens = [result.session_id, result.access_token, result.refresh_token, result.id_token, result.expires_in]
|
|
38
|
+
raise Error.client("passkey_session_missing") if tokens.any?(&:nil?)
|
|
39
|
+
|
|
40
|
+
PasskeySession.new(authentication: result, session_id: result.session_id, access_token: result.access_token,
|
|
41
|
+
refresh_token: result.refresh_token, id_token: result.id_token, token_type: "Bearer",
|
|
42
|
+
expires_in: result.expires_in)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Renames one of the user's passkeys (trimmed, 1 to 64 characters, no control characters).
|
|
46
|
+
# Another user's passkey answers resource_not_found.
|
|
47
|
+
def rename_passkey(user, passkey, name)
|
|
48
|
+
path = [self.class.bounded(user, 64), self.class.bounded(passkey, 64)]
|
|
49
|
+
body = { "name" => self.class.bounded(name, 64) }
|
|
50
|
+
Models::EndUserFactor.from_json(@client.request("updateEndUserPasskey", path, nil, body))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OhAuthCat
|
|
4
|
+
# SSO sign-in helpers: build the authorization URL with PKCE and exchange the code.
|
|
5
|
+
class SsoHelpers
|
|
6
|
+
VERIFIER = /\A[A-Za-z0-9._~-]{43,128}\z/
|
|
7
|
+
DOMAIN = /\A(?=.{1,253}\z)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}\z/
|
|
8
|
+
ORG = /\Aorg_[0-9A-HJKMNP-TV-Z]{26}\z/
|
|
9
|
+
CONN = /\Aconn_[0-9A-HJKMNP-TV-Z]{26}\z/
|
|
10
|
+
|
|
11
|
+
def initialize(client)
|
|
12
|
+
@client = client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# RFC 7636 S256 challenge for a verifier.
|
|
16
|
+
def self.pkce_challenge(verifier)
|
|
17
|
+
raise Error.client("request_invalid") unless verifier.is_a?(String) && verifier.match?(VERIFIER)
|
|
18
|
+
|
|
19
|
+
[OpenSSL::Digest::SHA256.digest(verifier)].pack("m0").tr("+/", "-_").delete("=")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns { url:, code_verifier:, state: }. Keep the verifier and state server-side;
|
|
23
|
+
# compare the callback state in constant time before the exchange. An empty state
|
|
24
|
+
# is replaced with a random one.
|
|
25
|
+
def get_authorization_url(redirect_uri:, state: "", organization: nil, connection: nil, domain: nil, login_hint: nil)
|
|
26
|
+
invalid = Error.client("request_invalid")
|
|
27
|
+
raise invalid if @client.client_id.nil?
|
|
28
|
+
raise invalid unless redirect_uri.is_a?(String) && !redirect_uri.empty? && redirect_uri.length <= 2048 && !redirect_uri.include?("#")
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
uri = URI.parse(redirect_uri)
|
|
32
|
+
rescue URI::InvalidURIError
|
|
33
|
+
raise invalid
|
|
34
|
+
end
|
|
35
|
+
raise invalid unless %w[http https].include?(uri.scheme) && uri.host && !uri.host.empty? && uri.userinfo.nil?
|
|
36
|
+
raise invalid unless state.is_a?(String) && state.length <= 1024
|
|
37
|
+
raise invalid if organization && !organization.match?(ORG)
|
|
38
|
+
raise invalid if connection && !connection.match?(CONN)
|
|
39
|
+
raise invalid if domain && !domain.strip.match?(DOMAIN)
|
|
40
|
+
|
|
41
|
+
if login_hint
|
|
42
|
+
hint = login_hint.strip
|
|
43
|
+
parts = hint.split("@", -1)
|
|
44
|
+
raise invalid if hint.length > 320 || parts.length != 2 || parts[0].empty? || hint.match?(/[\s<>"]/) || !parts[1].match?(DOMAIN)
|
|
45
|
+
end
|
|
46
|
+
state = SecureRandom.urlsafe_base64(32, false) if state.empty?
|
|
47
|
+
verifier = SecureRandom.urlsafe_base64(32, false)
|
|
48
|
+
query = { "client_id" => @client.client_id, "redirect_uri" => redirect_uri, "response_type" => "code", "state" => state,
|
|
49
|
+
"code_challenge" => self.class.pkce_challenge(verifier), "code_challenge_method" => "S256" }
|
|
50
|
+
query["organization"] = organization if organization
|
|
51
|
+
query["connection"] = connection if connection
|
|
52
|
+
query["domain"] = domain if domain
|
|
53
|
+
query["login_hint"] = login_hint if login_hint
|
|
54
|
+
{ url: "#{@client.standby.snapshot(true).origin}/sso/authorize?#{URI.encode_www_form(query)}", code_verifier: verifier, state: state }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Exchanges the code once (never retried). Raises RestartSignInError when the user must sign in again.
|
|
58
|
+
def get_profile_and_token(code:, code_verifier:, redirect_uri:)
|
|
59
|
+
raise Error.client("request_invalid", "client_id") if @client.client_id.nil?
|
|
60
|
+
raise Error.client("request_invalid", "api_key") unless @client.api_key.start_with?("sk_")
|
|
61
|
+
raise Error.client("request_invalid", "code") unless code.is_a?(String) && !code.empty? && code.length <= 4096
|
|
62
|
+
raise Error.client("request_invalid", "redirect_uri") unless redirect_uri.is_a?(String) && !redirect_uri.empty? && redirect_uri.length <= 2048
|
|
63
|
+
|
|
64
|
+
self.class.pkce_challenge(code_verifier)
|
|
65
|
+
body = { "grant_type" => "authorization_code", "client_id" => @client.client_id, "code" => code,
|
|
66
|
+
"code_verifier" => code_verifier, "redirect_uri" => redirect_uri }
|
|
67
|
+
Models::SsoToken.from_json(@client.request("exchangeSsoCode", [], nil, body))
|
|
68
|
+
rescue Error => e
|
|
69
|
+
raise RestartSignInError, e if %w[invalid_grant flow_interrupted_retry pkce_verification_failed].include?(e.code) && !e.is_a?(RestartSignInError)
|
|
70
|
+
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
TOKEN_TYPES = %w[urn:ietf:params:oauth:token-type:jwt urn:ietf:params:oauth:token-type:id_token].freeze
|
|
75
|
+
|
|
76
|
+
# A trusted-token profile with the OhAuthCat access token issued for it.
|
|
77
|
+
TrustedTokenAccess = Struct.new(:profile, :access_token, :token_type, :expires_in, keyword_init: true)
|
|
78
|
+
|
|
79
|
+
# Exchanges a trusted third-party token for its profile and an OhAuthCat access token whose
|
|
80
|
+
# audience is client_id (the configured publishable key by default). The token's sub is the
|
|
81
|
+
# profile ID (tprof_...), not an end-user ID; exchanged identities are not billable.
|
|
82
|
+
def exchange_trusted_token_for_access(subject_token:, subject_token_type: TOKEN_TYPES.first, client_id: nil)
|
|
83
|
+
invalid = Error.client("request_invalid")
|
|
84
|
+
raise invalid unless @client.api_key.start_with?("sk_")
|
|
85
|
+
raise invalid unless subject_token.is_a?(String) && !subject_token.strip.empty? && subject_token.length <= 16_384
|
|
86
|
+
raise invalid unless TOKEN_TYPES.include?(subject_token_type)
|
|
87
|
+
|
|
88
|
+
audience = client_id.nil? ? @client.client_id : PasskeyHelpers.bounded(client_id, 256)
|
|
89
|
+
raise invalid if audience.nil?
|
|
90
|
+
|
|
91
|
+
body = { "grant_type" => "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token" => subject_token,
|
|
92
|
+
"subject_token_type" => subject_token_type, "client_id" => audience }
|
|
93
|
+
profile = Models::TrustedTokenProfile.from_json(@client.request("exchangeTrustedToken", [], nil, body))
|
|
94
|
+
raise Error.client("access_token_missing") if profile.access_token.nil? || profile.expires_in.nil?
|
|
95
|
+
|
|
96
|
+
TrustedTokenAccess.new(profile: profile, access_token: profile.access_token,
|
|
97
|
+
token_type: profile.token_type || "Bearer", expires_in: profile.expires_in)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OhAuthCat
|
|
4
|
+
# Edge failover to the standby hosts (api./auth.oauthcat.com). After `threshold` consecutive
|
|
5
|
+
# edge failures (timeouts, transport errors, HTTP 520-539) requests go to the standby origins;
|
|
6
|
+
# the primary is re-probed at most every 60 s and used again once healthy. A failure on the
|
|
7
|
+
# standby path switches back and blocks failover for the cooldown.
|
|
8
|
+
class Standby
|
|
9
|
+
DEFAULT_API = "https://api.ohauthcat.com"
|
|
10
|
+
DEFAULT_AUTH = "https://auth.ohauthcat.com"
|
|
11
|
+
STANDBY_API = "https://api.oauthcat.com"
|
|
12
|
+
STANDBY_AUTH = "https://auth.oauthcat.com"
|
|
13
|
+
PROBE_INTERVAL = 60.0
|
|
14
|
+
|
|
15
|
+
Snapshot = Struct.new(:origin, :on_standby, :revision, :retained, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
attr_reader :api, :auth, :fallback_api, :fallback_auth, :enabled
|
|
18
|
+
|
|
19
|
+
def initialize(api:, auth:, enabled:, fallback_api:, fallback_auth:, threshold:, cooldown:, clock:)
|
|
20
|
+
@api = api
|
|
21
|
+
@auth = auth
|
|
22
|
+
@fallback_api = self.class.trusted_origin(fallback_api || STANDBY_API)
|
|
23
|
+
@fallback_auth = self.class.trusted_origin(fallback_auth || STANDBY_AUTH)
|
|
24
|
+
custom = api != DEFAULT_API || auth != DEFAULT_AUTH
|
|
25
|
+
explicit = !fallback_api.nil? && !fallback_auth.nil?
|
|
26
|
+
raise ArgumentError, "invalid standby" unless [true, false, nil].include?(enabled)
|
|
27
|
+
raise ArgumentError, "custom primary origins require both standby origins" if enabled == true && custom && !explicit
|
|
28
|
+
|
|
29
|
+
@enabled = enabled != false && (!custom || explicit)
|
|
30
|
+
raise ArgumentError, "standby_threshold must be 1..100" unless threshold.is_a?(Integer) && threshold.between?(1, 100)
|
|
31
|
+
raise ArgumentError, "standby_cooldown must be 0.001..3600 s" unless cooldown.is_a?(Numeric) && cooldown >= 0.001 && cooldown <= 3600
|
|
32
|
+
if @enabled && (api == @fallback_api || auth == @fallback_auth)
|
|
33
|
+
raise ArgumentError, "standby origins must differ from primary origins"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@threshold = threshold
|
|
37
|
+
@cooldown = cooldown
|
|
38
|
+
@clock = clock
|
|
39
|
+
@lock = Mutex.new
|
|
40
|
+
@active = false
|
|
41
|
+
@failures = 0
|
|
42
|
+
@blocked_until = @probe_after = clock.call
|
|
43
|
+
@probing = false
|
|
44
|
+
@revision = 0
|
|
45
|
+
@primary_failure = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.trusted_origin(value)
|
|
49
|
+
uri = URI.parse(value.to_s)
|
|
50
|
+
local = uri.scheme == "http" && %w[localhost 127.0.0.1 ::1 [::1]].include?(uri.host)
|
|
51
|
+
unless (uri.scheme == "https" || local) && uri.host && !uri.host.empty? && uri.userinfo.nil? && uri.query.nil? &&
|
|
52
|
+
uri.fragment.nil? && ["", "/"].include?(uri.path.to_s) && uri.port != 0
|
|
53
|
+
raise ArgumentError, "invalid standby origin"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
value.to_s.chomp("/")
|
|
57
|
+
rescue URI::InvalidURIError
|
|
58
|
+
raise ArgumentError, "invalid standby origin"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Transport failures and Cloudflare 52x/53x, never API answers such as 429 or 503.
|
|
62
|
+
def self.edge_error?(error)
|
|
63
|
+
error.status.between?(520, 539) ||
|
|
64
|
+
(%w[transport_error request_timeout].include?(error.code) && ![429, 503].include?(error.status))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def snapshot(auth_host)
|
|
68
|
+
@lock.synchronize do
|
|
69
|
+
origin = if @active
|
|
70
|
+
auth_host ? @fallback_auth : @fallback_api
|
|
71
|
+
else
|
|
72
|
+
auth_host ? @auth : @api
|
|
73
|
+
end
|
|
74
|
+
Snapshot.new(origin: origin, on_standby: @active, revision: @revision, retained: @primary_failure)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def success(revision)
|
|
79
|
+
@lock.synchronize { @failures = 0 if revision == @revision && !@active }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def edge(revision, error)
|
|
83
|
+
@lock.synchronize do
|
|
84
|
+
return false if revision != @revision || !@enabled || @active
|
|
85
|
+
|
|
86
|
+
@failures += 1
|
|
87
|
+
return false if @failures < @threshold || @clock.call < @blocked_until
|
|
88
|
+
|
|
89
|
+
@active = true
|
|
90
|
+
@primary_failure = error
|
|
91
|
+
@probe_after = @clock.call + PROBE_INTERVAL
|
|
92
|
+
@revision += 1
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unavailable(revision)
|
|
98
|
+
@lock.synchronize do
|
|
99
|
+
next unless revision == @revision
|
|
100
|
+
|
|
101
|
+
@active = false
|
|
102
|
+
@failures = 0
|
|
103
|
+
@blocked_until = @clock.call + @cooldown
|
|
104
|
+
@revision += 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def begin_probe
|
|
109
|
+
@lock.synchronize do
|
|
110
|
+
return nil if !@active || @probing || @clock.call < @probe_after
|
|
111
|
+
|
|
112
|
+
@probing = true
|
|
113
|
+
@probe_after = @clock.call + PROBE_INTERVAL
|
|
114
|
+
@revision
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def finish_probe(revision, healthy)
|
|
119
|
+
@lock.synchronize do
|
|
120
|
+
@probing = false
|
|
121
|
+
if healthy && revision == @revision
|
|
122
|
+
@active = false
|
|
123
|
+
@failures = 0
|
|
124
|
+
@revision += 1
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OhAuthCat
|
|
4
|
+
# Webhook signature verification (OhAuthCat-Signature: t=<unix>,v1=<hex>). Verify the raw
|
|
5
|
+
# body bytes before parsing; comparison is constant-time.
|
|
6
|
+
module Webhooks
|
|
7
|
+
SIGNATURE_HEADER = "OhAuthCat-Signature"
|
|
8
|
+
DEFAULT_TOLERANCE = 300
|
|
9
|
+
MAX_SAFE = (2**53) - 1
|
|
10
|
+
|
|
11
|
+
# Verification failed; #code never contains secrets, headers or the body.
|
|
12
|
+
class VerificationError < StandardError
|
|
13
|
+
attr_reader :code
|
|
14
|
+
|
|
15
|
+
def initialize(code)
|
|
16
|
+
super("Webhook verification failed: #{code}")
|
|
17
|
+
@code = code
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def parse_header(header)
|
|
24
|
+
raise VerificationError, "header_missing" if !header.is_a?(String) || header.strip.empty?
|
|
25
|
+
|
|
26
|
+
timestamp = nil
|
|
27
|
+
signatures = []
|
|
28
|
+
header.split(",", -1).each do |part|
|
|
29
|
+
name, sep, value = part.strip.partition("=")
|
|
30
|
+
raise VerificationError, "header_invalid" if sep.empty?
|
|
31
|
+
|
|
32
|
+
case name
|
|
33
|
+
when "t"
|
|
34
|
+
raise VerificationError, "header_invalid" if timestamp || value.length > 20 || !value.match?(/\A[0-9]+\z/)
|
|
35
|
+
|
|
36
|
+
timestamp = value.to_i
|
|
37
|
+
raise VerificationError, "header_invalid" unless timestamp.positive? && timestamp <= MAX_SAFE
|
|
38
|
+
when "v1"
|
|
39
|
+
signature = value.downcase
|
|
40
|
+
signatures << signature if signature.bytesize == 64
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
raise VerificationError, "header_invalid" if timestamp.nil? || signatures.empty?
|
|
44
|
+
|
|
45
|
+
[timestamp, signatures]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Hex HMAC-SHA256 over "<timestamp>.<body>", keyed by the whole UTF-8 secret.
|
|
49
|
+
def signature_digest(raw_body, timestamp, secret)
|
|
50
|
+
raise VerificationError, "body_invalid" unless raw_body.is_a?(String)
|
|
51
|
+
raise VerificationError, "secret_missing" unless secret.is_a?(String)
|
|
52
|
+
|
|
53
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret.b, "#{timestamp}.".b + raw_body.b)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def verify_signature(raw_body, header, secrets, tolerance: DEFAULT_TOLERANCE, now: nil)
|
|
57
|
+
raise VerificationError, "body_invalid" unless raw_body.is_a?(String)
|
|
58
|
+
raise VerificationError, "options_invalid" unless tolerance.is_a?(Numeric) && tolerance.positive?
|
|
59
|
+
|
|
60
|
+
timestamp, signatures = parse_header(header)
|
|
61
|
+
current = now || Time.now.to_i
|
|
62
|
+
raise VerificationError, "timestamp_outside" if (current - timestamp).abs > tolerance
|
|
63
|
+
|
|
64
|
+
held = secrets.is_a?(String) ? [secrets] : secrets
|
|
65
|
+
raise VerificationError, "secret_missing" unless held.is_a?(Array) && held.all?(String)
|
|
66
|
+
|
|
67
|
+
matched = false
|
|
68
|
+
nonempty = false
|
|
69
|
+
held.each do |secret|
|
|
70
|
+
next if secret.empty?
|
|
71
|
+
|
|
72
|
+
nonempty = true
|
|
73
|
+
expected = signature_digest(raw_body, timestamp, secret)
|
|
74
|
+
signatures.each do |signature|
|
|
75
|
+
matched = OpenSSL.fixed_length_secure_compare(expected, signature) | matched
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
raise VerificationError, "secret_missing" unless nonempty
|
|
79
|
+
raise VerificationError, "signature_invalid" unless matched
|
|
80
|
+
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Verifies the signature, then returns the decoded event Hash.
|
|
85
|
+
def verify(raw_body, header, secrets, tolerance: DEFAULT_TOLERANCE, now: nil)
|
|
86
|
+
verify_signature(raw_body, header, secrets, tolerance: tolerance, now: now)
|
|
87
|
+
event = begin
|
|
88
|
+
JSON.parse(raw_body.dup.force_encoding(Encoding::UTF_8))
|
|
89
|
+
rescue JSON::ParserError, EncodingError
|
|
90
|
+
raise VerificationError, "event_invalid"
|
|
91
|
+
end
|
|
92
|
+
raise VerificationError, "event_invalid" unless event.is_a?(Hash) && event["id"].is_a?(String) && event["type"].is_a?(String)
|
|
93
|
+
|
|
94
|
+
event
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/ohauthcat.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
require_relative "ohauthcat/version"
|
|
10
|
+
require_relative "ohauthcat/errors"
|
|
11
|
+
require_relative "ohauthcat/route"
|
|
12
|
+
require_relative "ohauthcat/model"
|
|
13
|
+
require_relative "ohauthcat/models"
|
|
14
|
+
require_relative "ohauthcat/access_policy"
|
|
15
|
+
require_relative "ohauthcat/standby"
|
|
16
|
+
require_relative "ohauthcat/generated"
|
|
17
|
+
require_relative "ohauthcat/webhooks"
|
|
18
|
+
require_relative "ohauthcat/sso_helpers"
|
|
19
|
+
require_relative "ohauthcat/passkey_helpers"
|
|
20
|
+
require_relative "ohauthcat/client"
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ohauthcat
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OhAuthCat
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Server-side SDK for OhAuthCat: enterprise SSO (SAML 2.0 and OIDC), SCIM
|
|
13
|
+
directory sync, webhooks and the management API.'
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- LICENSE
|
|
19
|
+
- README.md
|
|
20
|
+
- THIRD_PARTY_LICENSES
|
|
21
|
+
- lib/ohauthcat.rb
|
|
22
|
+
- lib/ohauthcat/access_policy.rb
|
|
23
|
+
- lib/ohauthcat/client.rb
|
|
24
|
+
- lib/ohauthcat/errors.rb
|
|
25
|
+
- lib/ohauthcat/generated.rb
|
|
26
|
+
- lib/ohauthcat/model.rb
|
|
27
|
+
- lib/ohauthcat/models.rb
|
|
28
|
+
- lib/ohauthcat/passkey_helpers.rb
|
|
29
|
+
- lib/ohauthcat/route.rb
|
|
30
|
+
- lib/ohauthcat/sso_helpers.rb
|
|
31
|
+
- lib/ohauthcat/standby.rb
|
|
32
|
+
- lib/ohauthcat/version.rb
|
|
33
|
+
- lib/ohauthcat/webhooks.rb
|
|
34
|
+
homepage: https://ohauthcat.com
|
|
35
|
+
licenses:
|
|
36
|
+
- Nonstandard
|
|
37
|
+
metadata:
|
|
38
|
+
homepage_uri: https://ohauthcat.com
|
|
39
|
+
documentation_uri: https://docs.ohauthcat.com
|
|
40
|
+
rubygems_mfa_required: 'true'
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.2'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.6.9
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: 'OhAuthCat server SDK: enterprise SSO and SCIM directory sync'
|
|
58
|
+
test_files: []
|