zevauth 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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module ZevAuth
6
+ module Resources
7
+ # One page of a list, as the API returned it, plus `each` over its items.
8
+ # Responses keep the API's own field names (`"firstName"`), so what you
9
+ # read here matches the API reference exactly.
10
+ class Page
11
+ include Enumerable
12
+
13
+ attr_reader :data, :next_cursor, :total_count, :raw
14
+
15
+ def initialize(raw)
16
+ @raw = raw
17
+ @data = raw.fetch("data", [])
18
+ @has_more = raw.fetch("hasMore", false)
19
+ @next_cursor = raw["next"]
20
+ @total_count = raw["totalCount"]
21
+ end
22
+
23
+ def has_more?
24
+ @has_more
25
+ end
26
+
27
+ def each(&)
28
+ @data.each(&)
29
+ end
30
+ end
31
+
32
+ class Base
33
+ def initialize(client)
34
+ @client = client
35
+ end
36
+
37
+ private
38
+
39
+ def path(*segments)
40
+ "/#{segments.map { |s| ERB::Util.url_encode(s.to_s) }.join('/')}"
41
+ end
42
+
43
+ # Ruby-style keys in, the API's camelCase on the wire: `first_name:`
44
+ # is sent as `firstName`. Only the top level is converted; metadata
45
+ # you pass is your data and is sent exactly as given.
46
+ def wire(params)
47
+ params.to_h.each_with_object({}) do |(key, value), out|
48
+ name = key.to_s
49
+ name = name.gsub(/_([a-z])/) { Regexp.last_match(1).upcase } if name.include?("_")
50
+ out[name] = value
51
+ end
52
+ end
53
+ end
54
+
55
+ class Users < Base
56
+ # One page. `metadata:` filters on public metadata, ANDed:
57
+ # users.list(limit: 50, metadata: { plan: "pro" })
58
+ def list(limit: nil, after: nil, query: nil, metadata: nil)
59
+ Page.new(@client.get("/v1/users", { limit: limit, after: after, query: query, metadata: metadata }))
60
+ end
61
+
62
+ # Every user, page after page, as an Enumerator:
63
+ # client.users.each(metadata: { plan: "pro" }) { |user| ... }
64
+ # client.users.each.lazy.select { ... }.first(10)
65
+ def each(limit: nil, query: nil, metadata: nil, &block)
66
+ enumerator = Enumerator.new do |yielder|
67
+ after = nil
68
+ loop do
69
+ page = list(limit: limit, after: after, query: query, metadata: metadata)
70
+ page.each { |user| yielder << user }
71
+ break unless page.has_more? && page.next_cursor
72
+
73
+ after = page.next_cursor
74
+ end
75
+ end
76
+ block ? enumerator.each(&block) : enumerator
77
+ end
78
+
79
+ def get(user_id) = @client.get(path("v1", "users", user_id))
80
+ def create(**params) = @client.post("/v1/users", wire(params))
81
+ def update(user_id, **params) = @client.patch(path("v1", "users", user_id), wire(params))
82
+ def delete(user_id) = @client.delete(path("v1", "users", user_id))
83
+
84
+ # Blocks or unblocks; blocking ends their sessions.
85
+ # Returns { "user" => …, "sessionsRevoked" => n }.
86
+ def set_blocked(user_id, blocked)
87
+ @client.post("#{path('v1', 'users', user_id)}/blocked", { "blocked" => blocked })
88
+ end
89
+
90
+ # Ends every session the user has. Returns { "sessionsRevoked" => n }.
91
+ def revoke_sessions(user_id)
92
+ @client.post("#{path('v1', 'users', user_id)}/sessions/revoke")
93
+ end
94
+
95
+ # Removes their two-step verification, for support. Sessions stay
96
+ # unless `revoke_sessions: true`.
97
+ def reset_mfa(user_id, revoke_sessions: false)
98
+ @client.delete("#{path('v1', 'users', user_id)}/mfa", revoke_sessions ? { revokeSessions: "true" } : nil)
99
+ end
100
+ end
101
+
102
+ class Organizations < Base
103
+ def list_for_user(user_id) = @client.get("/v1/organizations", { userId: user_id })
104
+ def get(organization_id) = @client.get(path("v1", "organizations", organization_id))
105
+
106
+ # `created_by:` (a user id) is required; they become the owner.
107
+ def create(**params) = @client.post("/v1/organizations", wire(params))
108
+
109
+ # Includes `require_mfa:` to require two-step for every member.
110
+ def update(organization_id, **params) = @client.patch(path("v1", "organizations", organization_id), wire(params))
111
+ def delete(organization_id) = @client.delete(path("v1", "organizations", organization_id))
112
+
113
+ def add_member(organization_id, user_id:, role: nil)
114
+ @client.post("#{path('v1', 'organizations', organization_id)}/members", { "userId" => user_id, "role" => role }.compact)
115
+ end
116
+
117
+ def remove_member(organization_id, user_id)
118
+ @client.delete(path("v1", "organizations", organization_id, "members", user_id))
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module ZevAuth
7
+ # What every HTTP call in this gem goes through.
8
+ #
9
+ # Net::HTTP from the standard library, not Faraday: an auth library sits
10
+ # in every request path of an app, and each dependency it pulls in is one
11
+ # more version for the app to reconcile and one more thing to trust. The
12
+ # calls here are a handful of JSON requests, which Net::HTTP does well.
13
+ #
14
+ # Anything responding to `call(method:, url:, headers:, body:, timeout:)`
15
+ # and returning a Transport::Response can be passed instead, which is how
16
+ # the tests run without a network and how an app can route through its
17
+ # own HTTP stack.
18
+ class Transport
19
+ Response = Struct.new(:status, :headers, :body, keyword_init: true) do
20
+ # Header lookup that does not care how the server cased the name.
21
+ def header(name)
22
+ wanted = name.downcase
23
+ headers.each { |key, value| return value if key.to_s.downcase == wanted }
24
+ nil
25
+ end
26
+ end
27
+
28
+ # Raised for anything that means "no answer": DNS, refused, timeout.
29
+ class Failure < StandardError; end
30
+
31
+ def call(method:, url:, headers:, body: nil, timeout: 15)
32
+ uri = URI.parse(url)
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+ http.use_ssl = uri.scheme == "https"
35
+ http.open_timeout = timeout
36
+ http.read_timeout = timeout
37
+ http.write_timeout = timeout if http.respond_to?(:write_timeout=)
38
+
39
+ request = Net::HTTPGenericRequest.new(
40
+ method.to_s.upcase, !body.nil?, method.to_s.upcase != "HEAD", uri.request_uri, headers
41
+ )
42
+ request.body = body unless body.nil?
43
+
44
+ response = http.request(request)
45
+ Response.new(status: response.code.to_i, headers: response.to_hash.transform_values(&:first), body: response.body)
46
+ rescue Timeout::Error, SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError, Net::ProtocolError => e
47
+ raise Failure, e.message
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+
7
+ module ZevAuth
8
+ # Verifies a ZevAuth session token and returns its claims.
9
+ #
10
+ # The checks run in the order the server SDK contract fixes, each with
11
+ # its own code, so a Ruby app and a PHP app refuse the same bad token
12
+ # with the same reason:
13
+ #
14
+ # 1. shape malformed_token
15
+ # 2. alg is RS256 unsupported_algorithm (before any key is touched)
16
+ # 3. key by kid unknown_key (one refetch for rotation)
17
+ # 4. signature invalid_signature
18
+ # 5. iss invalid_issuer (read from discovery)
19
+ # 6. aud invalid_audience (the environment id)
20
+ # 7. exp / nbf token_expired / token_not_yet_valid (5 s skew)
21
+ #
22
+ # The algorithm is never taken from the token. `none` and `HS256` (which
23
+ # would let the public key be used as an HMAC secret) are refused at
24
+ # step 2, whatever key they claim.
25
+ class Verifier
26
+ ALGORITHM = "RS256"
27
+ DEFAULT_CLOCK_SKEW = 5
28
+
29
+ attr_reader :environment_id
30
+
31
+ # environment_id: required; tokens for any other environment are refused
32
+ # api_url: where discovery and keys are fetched from
33
+ # issuer: override the discovered issuer (required with jwks:)
34
+ # jwks: a local key set (Hash or JSON) instead of fetching
35
+ # clock_skew_seconds: tolerance on exp/nbf, default 5
36
+ # clock: -> Time (or Numeric seconds); injectable for tests
37
+ # transport: HTTP for discovery and keys; see Transport
38
+ # jwks_cooldown_seconds: minimum gap between unknown-kid refetches, default 30
39
+ def initialize(environment_id:, api_url: ZevAuth::DEFAULT_API_URL, issuer: nil, jwks: nil,
40
+ clock_skew_seconds: DEFAULT_CLOCK_SKEW, clock: nil, transport: nil, jwks_cooldown_seconds: 30)
41
+ if environment_id.nil? || environment_id.to_s.empty?
42
+ raise ConfigurationError.new(
43
+ "ZevAuth needs an environment id (env_test_… or env_live_…) to verify tokens.",
44
+ code: "missing_environment_id"
45
+ )
46
+ end
47
+
48
+ @environment_id = environment_id
49
+ @skew = clock_skew_seconds
50
+ @clock = clock || -> { Time.now }
51
+ if jwks
52
+ raise ConfigurationError.new("`issuer` is required when a local `jwks` is given.", code: "missing_issuer") unless issuer
53
+
54
+ @keys = KeySet.local(jwks)
55
+ else
56
+ @keys = RemoteKeySet.new(
57
+ api_url: api_url, environment_id: environment_id, transport: transport || Transport.new,
58
+ clock: @clock, cooldown_seconds: jwks_cooldown_seconds
59
+ )
60
+ end
61
+ @issuer = issuer
62
+ end
63
+
64
+ # Returns the verified claims as a Hash with String keys. Raises
65
+ # VerifyError with a contract code.
66
+ def verify(token)
67
+ header, payload, signing_input, signature = decode(token)
68
+
69
+ alg = header["alg"]
70
+ raise VerifyError.new("Tokens must be signed with RS256, not #{alg.inspect}.", code: "unsupported_algorithm") unless alg == ALGORITHM
71
+
72
+ key = @keys.find(header["kid"])
73
+ raise VerifyError.new("No signing key matches kid #{header['kid'].inspect}.", code: "unknown_key") unless key
74
+
75
+ unless key.verify(OpenSSL::Digest.new("SHA256"), signature, signing_input)
76
+ raise VerifyError.new("The token signature does not verify.", code: "invalid_signature")
77
+ end
78
+
79
+ check_claims(payload)
80
+ payload
81
+ rescue OpenSSL::PKey::PKeyError
82
+ raise VerifyError.new("The token signature does not verify.", code: "invalid_signature")
83
+ end
84
+
85
+ # "Now" in seconds, from the injected clock. The bridge uses it to size
86
+ # the session cookie to the token's remaining life.
87
+ def now_seconds
88
+ now = @clock.call
89
+ now.respond_to?(:to_f) ? now.to_f : Float(now)
90
+ end
91
+
92
+ private
93
+
94
+ def decode(token)
95
+ parts = token.to_s.split(".", -1)
96
+ raise malformed("A session token has three dot-separated parts.") unless parts.length == 3 && !parts[0].empty? && !parts[1].empty?
97
+
98
+ header = parse_segment(parts[0])
99
+ payload = parse_segment(parts[1])
100
+ raise malformed("The token header and payload must be JSON objects.") unless header.is_a?(Hash) && payload.is_a?(Hash)
101
+
102
+ # An empty or undecodable signature is decided after the algorithm:
103
+ # an `alg: none` token carries no signature, and must be refused as
104
+ # the wrong algorithm, not as a malformed token.
105
+ signature = parts[2].match?(/\A[A-Za-z0-9_-]+\z/) ? base64url(parts[2]) : ""
106
+ [header, payload, "#{parts[0]}.#{parts[1]}", signature]
107
+ end
108
+
109
+ def parse_segment(segment)
110
+ JSON.parse(base64url(segment))
111
+ rescue JSON::ParserError
112
+ raise malformed("A token segment is not valid JSON.")
113
+ end
114
+
115
+ def base64url(segment)
116
+ raise malformed("A token segment is not base64url.") unless segment.match?(/\A[A-Za-z0-9_-]+\z/)
117
+
118
+ Base64.urlsafe_decode64(segment + ("=" * ((4 - (segment.length % 4)) % 4)))
119
+ rescue ArgumentError
120
+ raise malformed("A token segment is not base64url.")
121
+ end
122
+
123
+ def check_claims(payload)
124
+ issuer = @issuer || @keys.discovered_issuer
125
+ unless payload["iss"] == issuer
126
+ raise VerifyError.new("The token was issued by #{payload['iss'].inspect}, not #{issuer}.",
127
+ code: "invalid_issuer")
128
+ end
129
+
130
+ unless audience_matches?(payload["aud"])
131
+ raise VerifyError.new(
132
+ "The token belongs to another environment (#{payload['aud'].inspect}), not #{@environment_id}.",
133
+ code: "invalid_audience"
134
+ )
135
+ end
136
+
137
+ now = now_seconds
138
+ exp = numeric_claim(payload, "exp", required: true)
139
+ nbf = numeric_claim(payload, "nbf")
140
+ raise VerifyError.new("The token expired.", code: "token_expired") if now - @skew >= exp
141
+ raise VerifyError.new("The token is not valid yet.", code: "token_not_yet_valid") if nbf && now + @skew < nbf
142
+
143
+ %w[sub sid].each do |claim|
144
+ raise malformed("The `#{claim}` claim must be a string.") unless payload[claim].is_a?(String)
145
+ end
146
+ end
147
+
148
+ # The server sends a string; a list is accepted per RFC 7519 as long as
149
+ # it contains the environment.
150
+ def audience_matches?(aud)
151
+ aud.is_a?(Array) ? aud.include?(@environment_id) : aud == @environment_id
152
+ end
153
+
154
+ def numeric_claim(payload, name, required: false)
155
+ value = payload[name]
156
+ return nil if value.nil? && !required
157
+ raise malformed("The `#{name}` claim must be a number.") unless value.is_a?(Numeric)
158
+
159
+ value
160
+ end
161
+
162
+ def malformed(message)
163
+ VerifyError.new(message, code: "malformed_token")
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZevAuth
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module ZevAuth
7
+ # Checks that a webhook delivery came from ZevAuth.
8
+ #
9
+ # Verify the RAW request body, exactly as received. A body parsed and
10
+ # re-encoded is different bytes, and correctly fails: in Rails that means
11
+ # `request.raw_post`, in Rack `env["rack.input"].read`, never `params`.
12
+ module Webhooks
13
+ SIGNATURE_HEADER = "zevauth-signature"
14
+ DEFAULT_TOLERANCE = 300
15
+
16
+ Event = Struct.new(:id, :type, :created_at, :data, :raw, keyword_init: true)
17
+
18
+ module_function
19
+
20
+ # Raises WebhookError unless `signature` (the `zevauth-signature`
21
+ # header) is a valid, current signature of `payload` under `secret`
22
+ # (your `whsec_…` endpoint secret, prefix included).
23
+ def verify(payload:, signature:, secret: nil, tolerance_seconds: DEFAULT_TOLERANCE, now: nil)
24
+ secret ||= ZevAuth.config.webhook_secret
25
+ raise ConfigurationError.new("A webhook secret (whsec_…) is required.", code: "missing_webhook_secret") if secret.to_s.empty?
26
+
27
+ timestamp, given = parse(signature)
28
+ current = (now || Time.now).to_i
29
+ if (current - timestamp).abs > tolerance_seconds
30
+ raise WebhookError.new(
31
+ "The webhook timestamp is outside the #{tolerance_seconds}s tolerance (a replay, or a clock far off).",
32
+ code: "webhook_timestamp_out_of_range"
33
+ )
34
+ end
35
+
36
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}")
37
+ return true if secure_compare(expected, given)
38
+
39
+ raise WebhookError.new(
40
+ "The webhook signature does not match. Check the secret, and that you verify the raw body.",
41
+ code: "webhook_signature_mismatch"
42
+ )
43
+ end
44
+
45
+ # Verifies, then returns the parsed Event.
46
+ def construct_event(payload:, signature:, secret: nil, tolerance_seconds: DEFAULT_TOLERANCE, now: nil)
47
+ verify(payload: payload, signature: signature, secret: secret, tolerance_seconds: tolerance_seconds, now: now)
48
+ event = JSON.parse(payload)
49
+ Event.new(id: event["id"], type: event["type"], created_at: event["createdAt"], data: event["data"], raw: event)
50
+ end
51
+
52
+ def parse(header)
53
+ parts = header.to_s.split(",").each_with_object({}) do |part, out|
54
+ at = part.index("=")
55
+ next unless at
56
+
57
+ out[part[0...at].strip] = part[(at + 1)..].strip
58
+ end
59
+ timestamp = parts["t"]
60
+ given = parts["v1"]
61
+ unless timestamp&.match?(/\A\d+\z/) && given && !given.empty?
62
+ raise WebhookError.new(
63
+ "The zevauth-signature header is malformed: expected t=<timestamp>,v1=<signature>.",
64
+ code: "webhook_malformed_signature"
65
+ )
66
+ end
67
+
68
+ [timestamp.to_i, given]
69
+ end
70
+
71
+ # Constant time, and exact: no case folding, so an uppercase copy of a
72
+ # valid signature is still refused.
73
+ def secure_compare(expected, given)
74
+ return false unless expected.bytesize == given.bytesize
75
+
76
+ OpenSSL.fixed_length_secure_compare(expected, given)
77
+ end
78
+ end
79
+ end
data/lib/zevauth.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "zevauth/version"
6
+ require_relative "zevauth/errors"
7
+ require_relative "zevauth/transport"
8
+ require_relative "zevauth/key_set"
9
+ require_relative "zevauth/verifier"
10
+ require_relative "zevauth/auth"
11
+ require_relative "zevauth/bridge"
12
+ require_relative "zevauth/authenticator"
13
+ require_relative "zevauth/webhooks"
14
+ require_relative "zevauth/client"
15
+ require_relative "zevauth/resources"
16
+
17
+ # ZevAuth for Ruby.
18
+ #
19
+ # ZevAuth.configure do |c|
20
+ # c.environment_id = ENV.fetch("ZEVAUTH_ENVIRONMENT_ID")
21
+ # c.secret_key = ENV.fetch("ZEVAUTH_SECRET_KEY")
22
+ # end
23
+ #
24
+ # auth = ZevAuth.get_auth(request) # never raises
25
+ # ZevAuth.require_auth(request, require_mfa: true)
26
+ # ZevAuth.client.users.get(auth.user_id)
27
+ module ZevAuth
28
+ DEFAULT_API_URL = "https://api.zevauth.net"
29
+
30
+ class Configuration
31
+ # Required to verify sessions: env_test_… or env_live_…
32
+ attr_accessor :environment_id
33
+ # For the backend API: sk_test_… or sk_live_…
34
+ attr_accessor :secret_key
35
+ # For the handshake page: pk_test_… or pk_live_…
36
+ attr_accessor :publishable_key
37
+ # For webhook verification: whsec_…
38
+ attr_accessor :webhook_secret
39
+ # Where the API lives. Default https://api.zevauth.net
40
+ attr_accessor :api_url
41
+ # What the browser SDK on the handshake page talks to, when it differs
42
+ # from its own default. Usually leave nil.
43
+ attr_accessor :frontend_api_url
44
+ # A local key set and issuer instead of fetching (tests, air-gapped).
45
+ attr_accessor :jwks, :issuer
46
+ # Tolerance on exp/nbf in seconds. Default 5.
47
+ attr_accessor :clock_skew_seconds
48
+ # -> Time; injectable for tests.
49
+ attr_accessor :clock
50
+ # HTTP; see ZevAuth::Transport.
51
+ attr_accessor :transport
52
+ # Session bridge (server-rendered apps); see ZevAuth::Bridge::Config.
53
+ attr_accessor :bridge
54
+ # Where the bridge endpoint is mounted; the handshake page posts here.
55
+ attr_accessor :bridge_path
56
+
57
+ # Each key falls back to the environment variable of the same name, so
58
+ # an app that sets them needs no configuration code at all. The names
59
+ # match every other ZevAuth server SDK.
60
+ def initialize
61
+ @environment_id = ENV.fetch("ZEVAUTH_ENVIRONMENT_ID", nil)
62
+ @secret_key = ENV.fetch("ZEVAUTH_SECRET_KEY", nil)
63
+ @publishable_key = ENV.fetch("ZEVAUTH_PUBLISHABLE_KEY", nil)
64
+ @webhook_secret = ENV.fetch("ZEVAUTH_WEBHOOK_SECRET", nil)
65
+ @api_url = ENV.fetch("ZEVAUTH_API_URL", DEFAULT_API_URL)
66
+ @clock_skew_seconds = Verifier::DEFAULT_CLOCK_SKEW
67
+ @bridge = Bridge::Config.new
68
+ @bridge_path = "/zevauth/session"
69
+ end
70
+ end
71
+
72
+ class << self
73
+ def config
74
+ @config ||= Configuration.new
75
+ end
76
+
77
+ def configure
78
+ yield config
79
+ reset!
80
+ config
81
+ end
82
+
83
+ # Rebuilds the cached verifier and client from the current config.
84
+ def reset!
85
+ @authenticator = nil
86
+ @client = nil
87
+ end
88
+
89
+ def authenticator
90
+ @authenticator ||= Authenticator.new(
91
+ Verifier.new(
92
+ environment_id: config.environment_id,
93
+ api_url: config.api_url,
94
+ issuer: config.issuer,
95
+ jwks: config.jwks,
96
+ clock_skew_seconds: config.clock_skew_seconds,
97
+ clock: config.clock,
98
+ transport: config.transport
99
+ ),
100
+ bridge: config.bridge
101
+ )
102
+ end
103
+
104
+ def client
105
+ @client ||= Client.new(
106
+ secret_key: config.secret_key,
107
+ api_url: config.api_url,
108
+ transport: config.transport
109
+ )
110
+ end
111
+
112
+ # Verifies a token and returns its claims; raises VerifyError.
113
+ def verify_token(token)
114
+ authenticator.verifier.verify(token)
115
+ end
116
+
117
+ # Who is signed in on this request (Rack env or request object).
118
+ # Never raises for a missing or bad token.
119
+ def get_auth(request)
120
+ authenticator.get_auth(request)
121
+ end
122
+
123
+ # The same, but raises AuthRefused (401/403) unless the request passes.
124
+ def require_auth(request, allow_pending_tasks: false, require_mfa: false)
125
+ authenticator.require_auth(request, allow_pending_tasks: allow_pending_tasks, require_mfa: require_mfa)
126
+ end
127
+
128
+ # [auth, handshake_rack_response_or_nil]; see Authenticator.
129
+ def authenticate_request(request)
130
+ authenticator.authenticate_request(request)
131
+ end
132
+ end
133
+ end
134
+
135
+ require_relative "zevauth/rack/middleware"
136
+ require_relative "zevauth/rails/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zevauth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ZevOp Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.7'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4'
33
+ description: |
34
+ Verifies ZevAuth session tokens (RS256, keys fetched and rotated for you),
35
+ reads who is signed in from a bearer token or the session-bridge cookie,
36
+ calls the ZevAuth backend API with your secret key, and checks webhook
37
+ signatures. Includes Rack middleware and an optional Rails engine.
38
+ email:
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - CHANGELOG.md
44
+ - LICENSE
45
+ - README.md
46
+ - lib/zevauth.rb
47
+ - lib/zevauth/auth.rb
48
+ - lib/zevauth/authenticator.rb
49
+ - lib/zevauth/bridge.rb
50
+ - lib/zevauth/client.rb
51
+ - lib/zevauth/errors.rb
52
+ - lib/zevauth/key_set.rb
53
+ - lib/zevauth/rack/middleware.rb
54
+ - lib/zevauth/rails.rb
55
+ - lib/zevauth/rails/config/routes.rb
56
+ - lib/zevauth/rails/controller.rb
57
+ - lib/zevauth/rails/railtie.rb
58
+ - lib/zevauth/resources.rb
59
+ - lib/zevauth/transport.rb
60
+ - lib/zevauth/verifier.rb
61
+ - lib/zevauth/version.rb
62
+ - lib/zevauth/webhooks.rb
63
+ homepage: https://docs.zevauth.com
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://docs.zevauth.com
68
+ documentation_uri: https://docs.zevauth.com/sdk/ruby
69
+ source_code_uri: https://github.com/zevop/zevauth-ruby
70
+ bug_tracker_uri: https://github.com/zevop/zevauth-ruby/issues
71
+ changelog_uri: https://github.com/zevop/zevauth-ruby/blob/main/CHANGELOG.md
72
+ rubygems_mfa_required: 'true'
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.1'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.5.3
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: 'ZevAuth for Ruby: verify sessions, call the backend API, check webhooks.'
92
+ test_files: []