Class: Basecamp::Oauth::Discovery

Inherits:
Object
  • Object
show all
Defined in:
lib/basecamp/oauth/discovery.rb

Overview

Fetches RFC 8414 OAuth 2 Authorization Server Metadata and binds the returned issuer to the requested issuer origin (hop 2 of resource-first discovery).

Defined Under Namespace

Classes: IssuerBindingError

Instance Method Summary collapse

Constructor Details

#initialize(http_client: nil, timeout: 10, max_body_bytes: Fetcher::DEFAULT_MAX_BODY_BYTES) ⇒ Discovery

Returns a new instance of Discovery.

Parameters:

  • http_client (Faraday::Connection, nil) (defaults to: nil)

    HTTP client (SSRF-hardened default if nil)

  • timeout (Integer) (defaults to: 10)

    request timeout in seconds (default: 10)

  • max_body_bytes (Integer) (defaults to: Fetcher::DEFAULT_MAX_BODY_BYTES)

    bounded read cap in bytes



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/basecamp/oauth/discovery.rb', line 24

def initialize(http_client: nil, timeout: 10, max_body_bytes: Fetcher::DEFAULT_MAX_BODY_BYTES)
  Fetcher.ensure_redirects_suppressed!(http_client) if http_client
  # Normalize before building the client and before the fetch computes its
  # wall-clock deadline: a non-finite/non-positive timeout must not disable
  # either bound (see Fetcher.normalize_timeout).
  @timeout = Fetcher.normalize_timeout(timeout)
  @http_client = http_client || Fetcher.build_client(@timeout)
  # Normalize the public cap to a finite non-negative Integer: a nil, float,
  # or Float::INFINITY would otherwise disable the streaming memory bound
  # (an infinite/undefined cap never trips +total > max_body_bytes+),
  # reintroducing an SSRF/OOM risk. Mirrors Resource#initialize.
  @max_body_bytes =
    if max_body_bytes.is_a?(Integer) && max_body_bytes >= 0
      max_body_bytes
    else
      Fetcher::DEFAULT_MAX_BODY_BYTES
    end
end

Instance Method Details

#discover(base_url) ⇒ Config

Discovers OAuth configuration from {base_url}/.well-known/oauth-authorization-server, binding the returned issuer to base_url by code-point (RFC 8414 §3.3/§4, no normalization beyond origin-root parsing). token_endpoint is required; authorization_endpoint is optional (device-only servers omit it).

Examples:

config = Basecamp::Oauth::Discovery.new.discover("https://launchpad.37signals.com")
config.token_endpoint # => "https://launchpad.37signals.com/authorization/token"

Parameters:

  • base_url (String)

    the OAuth server's issuer origin

Returns:

  • (Config)

    the OAuth server configuration

Raises:



57
58
59
60
61
62
# File 'lib/basecamp/oauth/discovery.rb', line 57

def discover(base_url)
  issuer_origin = Basecamp::Security.require_origin_root!(base_url, "OAuth discovery base URL")
  # Bind against the caller's raw +base_url+ (RFC 8414 §3.3, SPEC.md §16 "NO
  # normalization"); the normalized origin is only for the fetch URL.
  discover_and_bind(issuer_origin, base_url)
end

#discover_advertised(advertised_issuer) ⇒ Object

Resource-first entry: validate advertised_issuer as an origin root, then fetch from the normalized origin and bind the AS metadata issuer against the RAW advertised string by code-point. Routing and binding are distinct — an AS whose issuer matches what the resource advertised (e.g. a trailing slash or explicit default port) binds instead of being normalized away into a false issuer_mismatch. Validation happens HERE so there is NO public unvalidated-fetch entry point: the SSRF origin policy is always enforced.

Parameters:

  • advertised_issuer (String)

    the raw issuer string a resource advertised

Raises:



74
75
76
77
# File 'lib/basecamp/oauth/discovery.rb', line 74

def discover_advertised(advertised_issuer)
  issuer_origin = Basecamp::Security.require_origin_root!(advertised_issuer, "advertised issuer")
  discover_and_bind(issuer_origin, advertised_issuer)
end