Module: Leash::Auth
- Defined in:
- lib/leash/auth.rb
Overview
Cookie + Bearer-token + JWT extraction across Ruby web frameworks.
Mirrors the multi-framework strategy in ‘leash-sdk-ts/src/server/auth.ts` and `leash-sdk-python/leash/auth.py`. Designed to never raise during extraction — `extract_cookie` / `extract_bearer_token` return `nil` on any unexpected request shape so callers can branch cleanly.
Supported request shapes (0.4):
* Rack hash (`{"rack.input" => …, "HTTP_COOKIE" => …}`)
* Rails `ActionDispatch::Request` (`request.cookies`, `request.headers`)
* Sinatra `Sinatra::Request` (`request.cookies`, `request.env`)
* Hanami request (responds to `:get_header`)
* Anything quacking with `.cookies` / `.env` / `.headers` / `.get_header`
Does NOT require rails, sinatra, rack, or hanami — only stdlib + jwt.
Constant Summary collapse
- COOKIE_NAME =
"leash-auth"- AUTH_HEADER =
"authorization"
Class Method Summary collapse
-
.authenticated?(request) ⇒ Boolean
True when Auth.get_user would return a user.
- .build_user(payload) ⇒ Object private
- .decode_token(token) ⇒ Object private
-
.extract_bearer_token(request) ⇒ Object
Return the JWT off ‘Authorization: Bearer …` if present, else `nil`.
-
.extract_cookie(request, name = COOKIE_NAME) ⇒ Object
Return the named cookie value off any request shape, or ‘nil`.
-
.extract_token(request) ⇒ Object
Backwards-compat alias for the 0.3 internal method name.
- .from_cookie_header(request, name) ⇒ Object private
- .from_cookie_jar(request, name) ⇒ Object private
-
.get_user(request) ⇒ Object
Decode the request’s leash-auth cookie into a User.
-
.header_lookup(request, name) ⇒ Object
private
Case-insensitive header lookup against any mapping or headers-like object.
- .normalise_cookie_value(value) ⇒ Object private
- .parse_cookie_header(header, name = COOKIE_NAME) ⇒ Object private
Class Method Details
.authenticated?(request) ⇒ Boolean
True when get_user would return a user.
45 46 47 48 49 50 |
# File 'lib/leash/auth.rb', line 45 def authenticated?(request) get_user(request) true rescue AuthError false end |
.build_user(payload) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/leash/auth.rb', line 274 def build_user(payload) id = payload["id"] || payload["sub"] || payload["userId"] email = payload["email"] raise AuthError, "Token payload missing required fields (id/sub, email)" unless id && email User.new( id: id.to_s, email: email, name: payload["name"], picture: payload["picture"] ) end |
.decode_token(token) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/leash/auth.rb', line 259 def decode_token(token) secret = ENV["LEASH_JWT_SECRET"] decoded = if secret && !secret.empty? JWT.decode(token, secret, true, algorithms: ["HS256"]) else JWT.decode(token, nil, false) end decoded.first rescue JWT::ExpiredSignature raise AuthError, "Token has expired" rescue JWT::DecodeError => e raise AuthError, "Invalid token: #{e.}" end |
.extract_bearer_token(request) ⇒ Object
Return the JWT off ‘Authorization: Bearer …` if present, else `nil`. Never raises.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/leash/auth.rb', line 73 def extract_bearer_token(request) return nil if request.nil? raw = header_lookup(request, AUTH_HEADER) return nil unless raw.is_a?(String) parts = raw.split(/\s+/, 2) return nil unless parts.length == 2 scheme, token = parts return nil unless scheme.downcase == "bearer" stripped = token.strip return nil if stripped.empty? stripped rescue StandardError nil end |
.extract_cookie(request, name = COOKIE_NAME) ⇒ Object
Return the named cookie value off any request shape, or ‘nil`. Never raises — returns `nil` on unexpected shapes.
58 59 60 61 62 63 64 |
# File 'lib/leash/auth.rb', line 58 def (request, name = COOKIE_NAME) return nil if request.nil? (request, name) || (request, name) rescue StandardError nil end |
.extract_token(request) ⇒ Object
Backwards-compat alias for the 0.3 internal method name.
67 68 69 |
# File 'lib/leash/auth.rb', line 67 def extract_token(request) (request) end |
.from_cookie_header(request, name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/leash/auth.rb', line 126 def (request, name) raw = nil if request.respond_to?(:env) env = begin request.env rescue StandardError nil end if env.is_a?(Hash) raw = env["HTTP_COOKIE"] || env["rack.cookie"] || env["cookie"] end end if raw.nil? && request.is_a?(Hash) raw = request["HTTP_COOKIE"] || request["rack.cookie"] || request["cookie"] || request[:cookie] end if raw.nil? raw = header_lookup(request, "cookie") end return nil unless raw.is_a?(String) && !raw.empty? (raw, name) end |
.from_cookie_jar(request, name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/leash/auth.rb', line 98 def (request, name) return nil unless request.respond_to?(:cookies) = request. return nil if .nil? if .respond_to?(:[]) value = nil begin value = [name] rescue StandardError value = nil end if value.nil? && .respond_to?(:fetch) begin value = .fetch(name.to_sym, nil) rescue StandardError value = nil end end normalised = (value) return normalised unless normalised.nil? end nil end |
.get_user(request) ⇒ Object
Decode the request’s leash-auth cookie into a User.
36 37 38 39 40 41 42 |
# File 'lib/leash/auth.rb', line 36 def get_user(request) token = (request) raise AuthError, "Missing leash-auth cookie" if token.nil? || token.empty? payload = decode_token(token) build_user(payload) end |
.header_lookup(request, name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Case-insensitive header lookup against any mapping or headers-like object.
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'lib/leash/auth.rb', line 186 def header_lookup(request, name) lname = name.downcase # Direct `headers` accessor (Rails / Rack / Hanami) if request.respond_to?(:headers) h = begin request.headers rescue StandardError nil end if h # Try common variants [name, lname, "HTTP_#{name.upcase.tr('-', '_')}"].each do |key| begin val = h[key] return val if val.is_a?(String) && !val.empty? rescue StandardError next end end if h.respond_to?(:each) begin h.each do |k, v| return v if k.respond_to?(:downcase) && k.downcase == lname && v.is_a?(String) end rescue StandardError # fall through end end end end # `get_header` accessor (Rack::Request / Hanami) if request.respond_to?(:get_header) begin val = request.get_header("HTTP_#{name.upcase.tr('-', '_')}") return val if val.is_a?(String) && !val.empty? rescue StandardError # ignore end begin val = request.get_header(name) return val if val.is_a?(String) && !val.empty? rescue StandardError # ignore end end # Raw `env` hash (Rack) if request.respond_to?(:env) env = begin request.env rescue StandardError nil end if env.is_a?(Hash) val = env["HTTP_#{name.upcase.tr('-', '_')}"] return val if val.is_a?(String) && !val.empty? end end # Caller passed a plain Hash of headers / env directly if request.is_a?(Hash) ["HTTP_#{name.upcase.tr('-', '_')}", name, lname, name.capitalize].each do |key| val = request[key] return val if val.is_a?(String) && !val.empty? end end nil end |
.normalise_cookie_value(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/leash/auth.rb', line 170 def (value) return nil if value.nil? return value if value.is_a?(String) && !value.empty? return value.value if value.respond_to?(:value) && value.value.is_a?(String) begin candidate = value["value"] return candidate if candidate.is_a?(String) && !candidate.empty? rescue StandardError nil end nil end |
.parse_cookie_header(header, name = COOKIE_NAME) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/leash/auth.rb', line 157 def (header, name = COOKIE_NAME) return nil if header.nil? header.split(";").each do |pair| k, v = pair.strip.split("=", 2) next unless k == name return v.nil? ? nil : v end nil end |