Module: Basecamp::Oauth
- Defined in:
- lib/basecamp/oauth.rb,
lib/basecamp/oauth/pkce.rb,
lib/basecamp/oauth/token.rb,
lib/basecamp/oauth/config.rb,
lib/basecamp/oauth/fetcher.rb,
lib/basecamp/oauth/exchange.rb,
lib/basecamp/oauth/resource.rb,
lib/basecamp/oauth/discovery.rb,
lib/basecamp/oauth/device_flow.rb,
lib/basecamp/oauth/oauth_error.rb,
lib/basecamp/oauth/refresh_request.rb,
lib/basecamp/oauth/discovery_result.rb,
lib/basecamp/oauth/exchange_request.rb,
lib/basecamp/oauth/device_flow_error.rb,
lib/basecamp/oauth/device_authorization.rb,
lib/basecamp/oauth/discovery_selection_error.rb,
lib/basecamp/oauth/protected_resource_metadata.rb
Overview
OAuth 2 module for Basecamp SDK.
Provides OAuth discovery, token exchange, and token refresh functionality. Supports both standard OAuth 2 and Basecamp's Launchpad legacy format.
Defined Under Namespace
Modules: DeviceFlow, Fetcher, Pkce Classes: Config, DeviceAuthorization, DeviceFlowError, Discovery, DiscoveryResult, DiscoverySelectionError, Exchange, ExchangeRequest, OauthError, ProtectedResourceMetadata, RefreshRequest, Resource, Token
Constant Summary collapse
- LAUNCHPAD_BASE_URL =
"https://launchpad.37signals.com"- FALLBACK_RESOURCE_DISCOVERY_FAILED =
Soft fallback reasons — the ONLY two outcomes under which discover_from_resource yields a fallback (Launchpad) rather than a selected config. Every other failure raises DiscoverySelectionError.
"resource_discovery_failed"- FALLBACK_NO_AS_ADVERTISED =
"no_as_advertised"
Class Method Summary collapse
-
.discover(base_url, timeout: 10) ⇒ Config
Discovers RFC 8414 Authorization Server Metadata and binds
issuertobase_urlby code-point. -
.discover_from_resource(resource_origin, expected_issuer: nil, timeout: 10) ⇒ DiscoveryResult
Resource-first discovery orchestrator (SPEC.md §16).
- .discover_launchpad(timeout: 10) ⇒ Object
-
.discover_protected_resource(resource_origin, timeout: 10) ⇒ ProtectedResourceMetadata
Discovers RFC 9728 protected-resource metadata for a resource origin.
- .exchange_code(token_endpoint:, code:, redirect_uri:, client_id:, client_secret: nil, code_verifier: nil, use_legacy_format: false, timeout: 30) ⇒ Object
-
.perform_device_login(config:, client_id:, display:, scope: nil, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT) ⇒ Token
Runs the full device authorization grant against an already-selected Config (RFC 8628).
-
.poll_device_token(token_endpoint:, client_id:, device_code:, interval:, expires_in:, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT, deadline_at: nil) ⇒ Token
Polls the token endpoint for a device grant (RFC 8628 §3.5).
- .refresh_token(token_endpoint:, refresh_token:, client_id: nil, client_secret: nil, use_legacy_format: false, timeout: 30, resource: nil) ⇒ Object
-
.request_device_authorization(device_authorization_endpoint:, client_id:, scope: nil, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT) ⇒ DeviceAuthorization
Requests an RFC 8628 device/user code pair.
- .token_expired?(token, buffer_seconds = 60) ⇒ Boolean
Class Method Details
.discover(base_url, timeout: 10) ⇒ Config
Discovers RFC 8414 Authorization Server Metadata and binds issuer to
base_url by code-point.
61 62 63 |
# File 'lib/basecamp/oauth.rb', line 61 def self.discover(base_url, timeout: 10) Discovery.new(timeout: timeout).discover(base_url) end |
.discover_from_resource(resource_origin, expected_issuer: nil, timeout: 10) ⇒ DiscoveryResult
Resource-first discovery orchestrator (SPEC.md §16). Composes RFC 9728 (resource metadata) and RFC 8414 (AS metadata) and applies the stage-sensitive fallback state machine.
Returns a DiscoveryResult that is either selected (a bound AS config)
or a soft fallback whose reason is resource_discovery_failed or
no_as_advertised ONLY. Every hard failure raises
DiscoverySelectionError — callers MUST NOT convert a raise into a
Launchpad request.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/basecamp/oauth.rb', line 97 def self.discover_from_resource(resource_origin, expected_issuer: nil, timeout: 10) # Origin-root validation of the *caller's* input is a usage error — let it # propagate as-is (not a soft fallback). # Hop 1: resource metadata. Any failure here is soft (before selection). # Pass the RAW resource_origin so binding is code-point-exact against the # caller's identifier (SPEC.md §16); discover_protected_resource normalizes # only its fetch URL. A malformed caller origin raises UsageError → re-raised. resource = begin discover_protected_resource(resource_origin, timeout: timeout) rescue Basecamp::UsageError raise rescue OauthError nil end if resource.nil? DiscoveryResult.fallback(FALLBACK_RESOURCE_DISCOVERY_FAILED) else select_and_bind(resource. || [], expected_issuer, timeout) end end |
.discover_launchpad(timeout: 10) ⇒ Object
65 66 67 |
# File 'lib/basecamp/oauth.rb', line 65 def self.discover_launchpad(timeout: 10) discover(LAUNCHPAD_BASE_URL, timeout: timeout) end |
.discover_protected_resource(resource_origin, timeout: 10) ⇒ ProtectedResourceMetadata
Discovers RFC 9728 protected-resource metadata for a resource origin.
74 75 76 |
# File 'lib/basecamp/oauth.rb', line 74 def self.discover_protected_resource(resource_origin, timeout: 10) Resource.new(timeout: timeout).discover(resource_origin) end |
.exchange_code(token_endpoint:, code:, redirect_uri:, client_id:, client_secret: nil, code_verifier: nil, use_legacy_format: false, timeout: 30) ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/basecamp/oauth.rb', line 119 def self.exchange_code( token_endpoint:, code:, redirect_uri:, client_id:, client_secret: nil, code_verifier: nil, use_legacy_format: false, timeout: 30 ) request = ExchangeRequest.new( token_endpoint: token_endpoint, code: code, redirect_uri: redirect_uri, client_id: client_id, client_secret: client_secret, code_verifier: code_verifier, use_legacy_format: use_legacy_format ) Exchange.new(timeout: timeout).exchange(request) end |
.perform_device_login(config:, client_id:, display:, scope: nil, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT) ⇒ Token
Runs the full device authorization grant against an already-selected Config (RFC 8628). Thin delegator to Basecamp::Oauth::DeviceFlow.perform_device_login.
184 185 186 187 188 189 190 191 192 193 |
# File 'lib/basecamp/oauth.rb', line 184 def self.perform_device_login( config:, client_id:, display:, scope: nil, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT ) DeviceFlow.perform_device_login( config: config, client_id: client_id, display: display, scope: scope, clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout ) end |
.poll_device_token(token_endpoint:, client_id:, device_code:, interval:, expires_in:, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT, deadline_at: nil) ⇒ Token
Polls the token endpoint for a device grant (RFC 8628 §3.5). Thin delegator
to Basecamp::Oauth::DeviceFlow.poll_device_token; clock, sleeper, and cancelled are
injectable for tests.
166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/basecamp/oauth.rb', line 166 def self.poll_device_token( token_endpoint:, client_id:, device_code:, interval:, expires_in:, clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER, cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT, deadline_at: nil ) DeviceFlow.poll_device_token( token_endpoint: token_endpoint, client_id: client_id, device_code: device_code, interval: interval, expires_in: expires_in, clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout, deadline_at: deadline_at ) end |
.refresh_token(token_endpoint:, refresh_token:, client_id: nil, client_secret: nil, use_legacy_format: false, timeout: 30, resource: nil) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/basecamp/oauth.rb', line 133 def self.refresh_token( token_endpoint:, refresh_token:, client_id: nil, client_secret: nil, use_legacy_format: false, timeout: 30, resource: nil ) request = RefreshRequest.new( token_endpoint: token_endpoint, refresh_token: refresh_token, client_id: client_id, client_secret: client_secret, use_legacy_format: use_legacy_format, resource: resource ) Exchange.new(timeout: timeout).refresh(request) end |
.request_device_authorization(device_authorization_endpoint:, client_id:, scope: nil, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT) ⇒ DeviceAuthorization
Requests an RFC 8628 device/user code pair. Thin delegator to Basecamp::Oauth::DeviceFlow.request_device_authorization.
154 155 156 157 158 159 |
# File 'lib/basecamp/oauth.rb', line 154 def self.(device_authorization_endpoint:, client_id:, scope: nil, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT) DeviceFlow.( device_authorization_endpoint: , client_id: client_id, scope: scope, timeout: timeout ) end |
.token_expired?(token, buffer_seconds = 60) ⇒ Boolean
146 147 148 |
# File 'lib/basecamp/oauth.rb', line 146 def self.token_expired?(token, buffer_seconds = 60) token.expired?(buffer_seconds) end |