Class: AmazonAds::OauthService
- Inherits:
-
Object
- Object
- AmazonAds::OauthService
- Defined in:
- app/services/amazon_ads/oauth_service.rb
Overview
Handles Amazon Advertising API OAuth 2.0 (Login with Amazon) for the
campaign sync that drives AmazonAdsCampaignSyncWorker.
System-level credential (account_id: nil) — one shared token for the
WarmlyYours Amazon Ads account, not a per-employee login.
Uses the "Heatwave" LWA security profile (credentials key
amazon_ads_api), approved for the advertising::campaign_management
scope. Like Microsoft there is no static-token fallback: Amazon access
tokens are short-lived (~1 hour) and only obtainable via OAuth.
Refresh-token expiry policy
Per the Amazon Ads Acceptable Use Policy effective 2026-07-30, refresh
tokens from consent granted after that date expire after 365 days and
require annual re-consent; consent granted before is grandfathered
(non-expiring). #exchange_code! stamps metadata['consented_at'] so the
token's age is always known, and AmazonAdsTokenRefreshWorker surfaces a
dead grant (invalid_grant) via OauthCredential#record_refresh_failure!
— reconnect at /amazon_ads/oauth/authorize when that happens.
Flow
- Crm::AmazonAdsOauthController#authorize redirects an admin to
#authorization_url. - Amazon redirects back to
/amazon_ads/oauth/callback?code=…. - #exchange_code! swaps the code for an access + refresh token,
stored in OauthCredential. - #access_token! refreshes lazily on expiry; the refresh token is
long-lived and reused (Amazon does not rotate it on refresh).
Defined Under Namespace
Classes: TokenRefreshError, TransportError
Constant Summary collapse
- PROVIDER =
OAuth provider key stored on OauthCredential#provider.
'amazon_ads'- AUTHORIZE_URL =
Endpoint the admin's browser is redirected to for consent (NA region).
'https://www.amazon.com/ap/oa'- TOKEN_HOST =
LWA token endpoint host — shared with SP-API, distinct from the Ads API host.
'https://api.amazon.com'- SCOPES =
Space-separated scopes requested during consent. Campaign management is
the only Ads scope the "Heatwave" app is approved for (no
advertising::audiences). 'advertising::campaign_management'
Instance Attribute Summary collapse
-
#client_id ⇒ String
readonly
The LWA client id — also sent as the
Amazon-Advertising-API-ClientIdheader on every Ads API call.
Instance Method Summary collapse
-
#access_token! ⇒ String
Return a valid access token, refreshing lazily when the stored one has expired.
-
#authorization_url(state:) ⇒ String
Amazon consent URL to redirect the admin to for one-time authorization.
-
#connected? ⇒ Boolean
Whether the OAuth flow has been connected.
-
#connection_status ⇒ Hash
DB-only connection status (no HTTP) — safe to call on every page load.
-
#disconnect! ⇒ Object
Remove the stored credential.
-
#exchange_code!(code) ⇒ OauthCredential
Exchange the authorization code for tokens and persist them.
-
#healthy? ⇒ Boolean
Health check: confirm we can produce a usable access token (lazy refresh included).
-
#initialize(account: nil) ⇒ OauthService
constructor
A new instance of OauthService.
-
#refresh! ⇒ OauthCredential
Exchange the stored refresh token for a fresh access token.
Constructor Details
#initialize(account: nil) ⇒ OauthService
Returns a new instance of OauthService.
65 66 67 68 69 |
# File 'app/services/amazon_ads/oauth_service.rb', line 65 def initialize(account: nil) @account = account @client_id = Heatwave::Configuration.fetch(:amazon_ads_api, :client_id) @client_secret = Heatwave::Configuration.fetch(:amazon_ads_api, :client_secret) end |
Instance Attribute Details
#client_id ⇒ String (readonly)
Returns the LWA client id — also sent as the
Amazon-Advertising-API-ClientId header on every Ads API call.
73 74 75 |
# File 'app/services/amazon_ads/oauth_service.rb', line 73 def client_id @client_id end |
Instance Method Details
#access_token! ⇒ String
Return a valid access token, refreshing lazily when the stored one has
expired. No static fallback — the OAuth flow must have been connected.
122 123 124 125 126 127 128 129 130 131 |
# File 'app/services/amazon_ads/oauth_service.rb', line 122 def access_token! credential = OauthCredential.for(PROVIDER, account: @account) unless credential raise TokenRefreshError, 'Amazon Ads not connected: run the OAuth flow at /amazon_ads/oauth/authorize' end refresh! if credential.token_expired? credential.reload.access_token end |
#authorization_url(state:) ⇒ String
Amazon consent URL to redirect the admin to for one-time authorization.
79 80 81 82 83 84 85 86 87 88 |
# File 'app/services/amazon_ads/oauth_service.rb', line 79 def (state:) query = { client_id: @client_id, scope: SCOPES, response_type: 'code', redirect_uri: redirect_uri, state: state }.to_query "#{AUTHORIZE_URL}?#{query}" end |
#connected? ⇒ Boolean
Returns whether the OAuth flow has been connected.
134 135 136 |
# File 'app/services/amazon_ads/oauth_service.rb', line 134 def connected? OauthCredential.for(PROVIDER, account: @account).present? end |
#connection_status ⇒ Hash
DB-only connection status (no HTTP) — safe to call on every page load.
141 142 143 144 145 146 147 148 149 |
# File 'app/services/amazon_ads/oauth_service.rb', line 141 def connection_status credential = OauthCredential.for(PROVIDER, account: @account) if credential healthy = credential.token_fresh? || credential.refresh_token.present? { connected: true, healthy: healthy, credential: credential, via: :oauth } else { connected: false, healthy: false, credential: nil, via: nil } end end |
#disconnect! ⇒ Object
Remove the stored credential.
152 153 154 |
# File 'app/services/amazon_ads/oauth_service.rb', line 152 def disconnect! OauthCredential.destroy_by(provider: PROVIDER, account_id: @account&.id) end |
#exchange_code!(code) ⇒ OauthCredential
Exchange the authorization code for tokens and persist them.
95 96 97 98 99 100 101 102 |
# File 'app/services/amazon_ads/oauth_service.rb', line 95 def exchange_code!(code) response = token_request( grant_type: 'authorization_code', code: code, redirect_uri: redirect_uri ) persist_tokens!(response, consented: true) end |
#healthy? ⇒ Boolean
Health check: confirm we can produce a usable access token (lazy refresh
included). DB + token endpoint only — no Ads API call.
160 161 162 163 164 |
# File 'app/services/amazon_ads/oauth_service.rb', line 160 def healthy? access_token!.present? rescue TokenRefreshError, StandardError false end |
#refresh! ⇒ OauthCredential
Exchange the stored refresh token for a fresh access token.
108 109 110 111 112 113 114 115 |
# File 'app/services/amazon_ads/oauth_service.rb', line 108 def refresh! credential = OauthCredential.for(PROVIDER, account: @account) raise TokenRefreshError, 'No Amazon Ads credential found' unless credential raise TokenRefreshError, 'No refresh token available' if credential.refresh_token.blank? response = token_request(grant_type: 'refresh_token', refresh_token: credential.refresh_token) persist_tokens!(response, credential: credential) end |