Class: Basecamp::OauthService

Inherits:
Object
  • Object
show all
Defined in:
app/services/basecamp/oauth_service.rb

Overview

Handles Basecamp 4 OAuth 2.0 token exchange and refresh.

Basecamp uses standard OAuth 2.0 Authorization Code flow:

  1. Redirect user to authorization_url
  2. Basecamp redirects back with ?code=...
  3. Exchange code for access_token + refresh_token
  4. When token expires, use refresh_token to get a new pair

Reference: https://github.com/basecamp/api/blob/master/sections/authentication.md

Defined Under Namespace

Classes: TokenRefreshError

Constant Summary collapse

AUTHORIZE_URL =
'https://launchpad.37signals.com/authorization/new'
TOKEN_URL =
'https://launchpad.37signals.com/authorization/token'
SITE_URL =
'https://launchpad.37signals.com'
PROVIDER =
'basecamp'

Instance Method Summary collapse

Constructor Details

#initialize(account: nil) ⇒ OauthService

Returns a new instance of OauthService.

Parameters:

  • account (Account, nil) (defaults to: nil)

    the CRM account whose Basecamp credential to manage
    Pass nil for system-level (legacy/MCP server) credentials.



24
25
26
27
28
29
30
# File 'app/services/basecamp/oauth_service.rb', line 24

def initialize(account: nil)
  @account       = 
  @client_id     = Heatwave::Configuration.fetch(:basecamp, :client_id)
  @client_secret = Heatwave::Configuration.fetch(:basecamp, :client_secret)
  @redirect_url  = Heatwave::Configuration.fetch(:basecamp, :redirect_url)
  @basecamp_account_id = Heatwave::Configuration.fetch(:basecamp, :account_id)
end

Instance Method Details

#access_token!String

Get a valid access token, refreshing if expired.

Returns:

  • (String)

    the access_token

Raises:



87
88
89
90
91
92
93
# File 'app/services/basecamp/oauth_service.rb', line 87

def access_token!
  credential = OauthCredential.for(PROVIDER, account: @account)
  raise TokenRefreshError, 'No Basecamp credential found' unless credential

  refresh! if credential.token_expired?
  credential.reload.access_token
end

#account_idString

The configured Basecamp account ID for API calls.

Returns:

  • (String)


97
98
99
# File 'app/services/basecamp/oauth_service.rb', line 97

def 
  @basecamp_account_id
end

#authorization_url(state: nil) ⇒ String

URL to redirect the user to for authorization.
Uses standard OAuth 2.0 response_type per Basecamp API docs.

Parameters:

  • state (String) (defaults to: nil)

    CSRF token

Returns:

  • (String)


36
37
38
39
40
41
42
43
44
# File 'app/services/basecamp/oauth_service.rb', line 36

def authorization_url(state: nil)
  params = {
    response_type: 'code',
    client_id: @client_id,
    redirect_uri: @redirect_url
  }
  params[:state] = state if state.present?
  "#{AUTHORIZE_URL}?#{params.to_query}"
end

#connected?Boolean

Whether this CRM account has a stored Basecamp credential.

Returns:

  • (Boolean)


103
104
105
# File 'app/services/basecamp/oauth_service.rb', line 103

def connected?
  OauthCredential.for(PROVIDER, account: @account).present?
end

#disconnect!Object

Remove the stored credential for this account (disconnect).



108
109
110
# File 'app/services/basecamp/oauth_service.rb', line 108

def disconnect!
  OauthCredential.where(provider: PROVIDER, account_id: @account&.id).destroy_all
end

#exchange_code!(code) ⇒ OauthCredential

Exchange the authorization code for tokens and persist them.

Parameters:

  • code (String)

    authorization code from callback

Returns:



49
50
51
52
53
54
55
56
57
58
59
# File 'app/services/basecamp/oauth_service.rb', line 49

def exchange_code!(code)
  response = token_request(
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: @redirect_url,
    client_id: @client_id,
    client_secret: @client_secret
  )

  persist_tokens!(response)
end

#refresh!OauthCredential

Refresh the stored access token.

Returns:

Raises:



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/services/basecamp/oauth_service.rb', line 64

def refresh!
  credential = OauthCredential.for(PROVIDER, account: @account)
  raise TokenRefreshError, 'No Basecamp 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,
    client_id: @client_id,
    client_secret: @client_secret
  )

  attrs = {
    access_token: response['access_token'],
    expires_at: response['expires_in'] ? response['expires_in'].to_i.seconds.from_now : nil
  }
  attrs[:refresh_token] = response['refresh_token'] if response['refresh_token'].present?
  credential.update!(attrs)
  credential
end