Class: RackJwtVerifier::Middleware

Inherits:
Object
  • Object
show all
Defined in:
lib/rack_jwt_verifier/middleware.rb

Overview

The primary middleware class responsible for intercepting requests, extracting the JWT, verifying it, and injecting the user's details into the Rack environment.

Constant Summary collapse

RACK_ENV_PAYLOAD_KEY =

The default key in the Rack environment used to store the verified JWT payload. This can be accessed by downstream applications (e.g., Rails controllers) to retrieve the authenticated user's details.

"rack_jwt_verifier.payload"
RETRY_AFTER_SECONDS =

Seconds a client is told to wait before retrying after a 503.

5
NULL_LOGGER =

Used when neither a :logger option nor env is available.

Logger.new(IO::NULL)
ERROR_RESPONSES =

Status and default plain-text body for each way a request can be refused. The reason symbol doubles as the error code in JSON bodies.

{
  missing_token: [401, "Unauthorized: Bearer token required."],
  invalid_token: [401, "Unauthorized: Invalid or expired JWT."],
  insufficient_scope: [403, "Forbidden: the token does not grant the required scope."],
  key_unavailable: [503, "Service Unavailable: could not fetch the token verification key."],
  replay_cache_unavailable: [503, "Service Unavailable: could not check the token for replay."]
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(app, options = {}) ⇒ Middleware

Returns a new instance of Middleware.

Options Hash (options):

  • :require_token (Boolean)

    Reject requests that carry no Bearer token (default: false, pass through).

  • :logger (Logger)

    Logger for verification failures (default: env, else silent).

  • :env_key (String)

    Rack env key that receives the payload (default: RACK_ENV_PAYLOAD_KEY).

  • :skip (Array<String, Regexp, #call>)

    Paths (exact string, regexp) or predicates on env that bypass the middleware.

  • :json_errors (Boolean)

    Render 401/503 bodies as JSON {"error", "error_description"} (default: plain text).

  • :on_error (#call)

    ->(env, reason, exception) { rack_response or nil } to customise refusals.

  • :require_scopes (Array<String>)

    Scopes every token must grant; a token lacking one gets a 403 with an insufficient_scope challenge. Implies :require_token.

  • :require_iss_aud (Boolean)

    Refuse to boot unless decode_options carries both :iss and :aud (default: true). false logs a warning instead.

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/rack_jwt_verifier/middleware.rb', line 54

def initialize(app, options = {})
  @app = app
  @logger = options[:logger]
  @env_key = options.fetch(:env_key, RACK_ENV_PAYLOAD_KEY)
  @skip = validate_skip_rules(Array(options[:skip]))
  @json_errors = options.fetch(:json_errors, false)
  @on_error = options[:on_error]
  @require_scopes = validate_required_scopes(options[:require_scopes])
  @require_token = resolve_require_token(options)

  # The Verifier instance is initialized with options (like public_key_url)
  # and is responsible for all crypto and key management.
  @verifier = Verifier.new(options)

  enforce_claim_policy(options)
end

Instance Method Details

#call(env) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rack_jwt_verifier/middleware.rb', line 71

def call(env)
  return @app.call(env) if skip?(env)

  token = extract_token(env)

  # With no token the request is passed down the stack and the application
  # decides how to treat the unauthenticated state — unless require_token
  # is set, in which case it is rejected here.
  unless token
    return error_response(env, :missing_token, nil) if @require_token

    return @app.call(env)
  end

  # Only the verification step is guarded: a JWT::DecodeError raised by the
  # downstream application must propagate, not be turned into a 401 here.
  begin
    # Use the Verifier to handle the complex crypto and validation logic
    payload = @verifier.verify(token)
  rescue JWT::DecodeError => e
    # Invalid signature, expired, bad claim: the client's problem.
    logger(env).warn { "rack_jwt_verifier: token rejected: #{e.message}" }
    return error_response(env, :invalid_token, e)
  rescue KeyFetchError => e
    # We could not obtain the key to check the token: our problem, not the
    # client's, so answer 503 rather than 401.
    logger(env).error { "rack_jwt_verifier: #{e.message}" }
    return error_response(env, :key_unavailable, e)
  rescue ReplayCacheError => e
    logger(env).error { "rack_jwt_verifier: #{e.message}" }
    return error_response(env, :replay_cache_unavailable, e)
  end

  missing = Scopes.missing(payload, @require_scopes)
  unless missing.empty?
    error = InsufficientScopeError.new(required: @require_scopes, missing: missing)
    logger(env).warn { "rack_jwt_verifier: #{error.message}" }
    return error_response(env, :insufficient_scope, error)
  end

  # On successful verification, store the payload in the Rack environment
  env[@env_key] = payload

  @app.call(env)
end