Class: MicrosoftAds::OauthService
- Inherits:
-
Object
- Object
- MicrosoftAds::OauthService
- Defined in:
- app/services/microsoft_ads/oauth_service.rb
Overview
Handles Microsoft Advertising (Bing Ads) OAuth 2.0 via the Microsoft
identity platform (Entra ID), for the campaign sync that drives
MicrosoftAdsCampaignSyncWorker and the server-side conversion upload in
ConversionReporter.
System-level credential (account_id: nil) — one shared token for the
WarmlyYours Microsoft Advertising account, not a per-employee login.
Differences from Pinterest/YouTube
- No static-token fallback. Microsoft access tokens are short-lived
(~1 hour) and only obtainable via OAuth, so every caller goes through
#access_token!, which refreshes lazily when the stored token expired. - Confidential client. The app is registered in Entra as a Web app
with a client secret — required for a server-side daemon that refreshes
tokens across our Dallas/Chicago containers (a public client's refresh
token is bound to the single device that minted it). - The long-lived refresh token is additionally exercised once a day by
MicrosoftAdsTokenRefreshWorker to surface a revoked grant before a
feature path hits it.
Flow
- Crm::MicrosoftAdsOauthController#authorize redirects an admin to
#authorization_url. - Microsoft redirects back to
/microsoft_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
reused until Microsoft rotates it (the new value is then persisted).
Defined Under Namespace
Classes: TokenRefreshError
Constant Summary collapse
- PROVIDER =
OAuth provider key stored on OauthCredential#provider.
'microsoft_ads'- LOGIN_HOST =
Microsoft identity platform v2.0 host.
'https://login.microsoftonline.com'- DEFAULT_TENANT =
Tenant segment of the authority.
commonsupports both work/school
(Entra) and personal Microsoft accounts, and is correct for a
multi-tenant + personal-accounts app registration (Microsoft's
recommended Bing Ads type). For a single-tenant ("My organization only")
registration, pinmicrosoft_ads.tenant_idto the directory (tenant) ID
so the authority is tenant-scoped —commonrejects an org-only app. 'common'- SCOPES =
Space-separated scopes.
msads.managegrants Microsoft Advertising API
access;offline_accessis required to receive a refresh token. 'https://ads.microsoft.com/msads.manage offline_access'
Instance Method Summary collapse
-
#access_token! ⇒ String
Return a valid access token, refreshing lazily when the stored one has expired.
-
#authorization_url(state:) ⇒ String
Microsoft 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 + refresh token pair.
Constructor Details
#initialize(account: nil) ⇒ OauthService
Returns a new instance of OauthService.
61 62 63 64 65 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 61 def initialize(account: nil) @account = account @client_id = Heatwave::Configuration.fetch(:microsoft_ads, :app_id) @client_secret = Heatwave::Configuration.fetch(:microsoft_ads, :app_secret_key) end |
Instance Method Details
#access_token! ⇒ String
Return a valid access token, refreshing lazily when the stored one has
expired. Unlike Pinterest there is no static fallback — the OAuth flow
must have been connected.
120 121 122 123 124 125 126 127 128 129 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 120 def access_token! credential = OauthCredential.for(PROVIDER, account: @account) unless credential raise TokenRefreshError, 'Microsoft Advertising not connected: run the OAuth flow at /microsoft_ads/oauth/authorize' end refresh! if credential.token_expired? credential.reload.access_token end |
#authorization_url(state:) ⇒ String
Microsoft consent URL to redirect the admin to for one-time authorization.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 71 def (state:) query = { client_id: @client_id, scope: SCOPES, response_type: 'code', redirect_uri: redirect_uri, response_mode: 'query', state: state, # Force the account chooser so the admin connects the intended Super # Admin user (the one that owns the developer token), not whatever # Microsoft session happens to be active in the browser. prompt: 'select_account' }.to_query "#{LOGIN_HOST}/#{tenant}/oauth2/v2.0/authorize?#{query}" end |
#connected? ⇒ Boolean
Returns whether the OAuth flow has been connected.
132 133 134 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 132 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.
139 140 141 142 143 144 145 146 147 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 139 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.
150 151 152 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 150 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.
92 93 94 95 96 97 98 99 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 92 def exchange_code!(code) response = token_request( grant_type: 'authorization_code', code: code, redirect_uri: redirect_uri ) persist_tokens!(response) end |
#healthy? ⇒ Boolean
Health check: confirm we can produce a usable access token (lazy refresh
included). DB + token endpoint only — no SOAP API call.
158 159 160 161 162 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 158 def healthy? access_token!.present? rescue TokenRefreshError, StandardError false end |
#refresh! ⇒ OauthCredential
Exchange the stored refresh token for a fresh access + refresh token pair.
105 106 107 108 109 110 111 112 |
# File 'app/services/microsoft_ads/oauth_service.rb', line 105 def refresh! credential = OauthCredential.for(PROVIDER, account: @account) raise TokenRefreshError, 'No Microsoft Advertising 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 |