Class: Ask::Auth::Providers::OAuth

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/auth/providers/oauth.rb

Overview

Authorization-code PKCE OAuth flow for interactive credential authorization (e.g. bring-your-own-subscription).

provider = Ask::Auth::Providers::OAuth.new(
client_id: "...", authorize_url: "...", token_url: "...",
redirect_uri: "https://app.example/oauth/callback", scope: "..."
)
url = provider.authorize_url(user: current_user)
# redirect user to url; the callback receives ?code=...&state=...
tokens = provider.authorize!(user: current_user, code: params[:code])
# { token:, refresh_token:, expires_at:, raw: {...} }

The code verifier is generated during #authorize_url. Persist it via a storage object that responds to #store(name, user:, value:) / #fetch(name, user:) — the app implements it (e.g. on its OAuth state table) — or pass code_verifier: back into #authorize! yourself.

Subclasses override #parse_token_response to add provider-specific fields (e.g. an account id from the id_token).

Direct Known Subclasses

DeviceOAuth, OpenaiCodex

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(storage: nil, client_id: nil, authorize_url: nil, token_url: nil, redirect_uri: nil, scope: nil, http: Ask::Auth::OAuth::HTTP) ⇒ OAuth

Returns a new instance of OAuth.



34
35
36
37
38
39
40
41
42
43
# File 'lib/ask/auth/providers/oauth.rb', line 34

def initialize(storage: nil, client_id: nil, authorize_url: nil, token_url: nil,
  redirect_uri: nil, scope: nil, http: Ask::Auth::OAuth::HTTP)
  @storage = storage
  @client_id = client_id
  @authorize_url = authorize_url
  @token_url = token_url
  @redirect_uri = redirect_uri
  @scope = scope
  @http = http
end

Instance Attribute Details

#client_idObject (readonly)

Returns the value of attribute client_id.



32
33
34
# File 'lib/ask/auth/providers/oauth.rb', line 32

def client_id
  @client_id
end

#redirect_uriObject (readonly)

Returns the value of attribute redirect_uri.



32
33
34
# File 'lib/ask/auth/providers/oauth.rb', line 32

def redirect_uri
  @redirect_uri
end

Instance Method Details

#authorize!(user:, code:, code_verifier: nil, redirect_uri: nil) ⇒ Object

Exchange an authorization code for tokens.

Returns { token:, refresh_token:, expires_at:, raw: ... } (plus any subclass-added keys). Raises OAuthError on transport or provider errors.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ask/auth/providers/oauth.rb', line 93

def authorize!(user:, code:, code_verifier: nil, redirect_uri: nil)
  verifier = code_verifier || fetch_oauth_state(user, :verifier)
  if verifier.to_s.empty?
    raise OAuthError, "missing code verifier — pass code_verifier or persist it via a storage object"
  end

  body = token_exchange(
    grant_type: "authorization_code",
    code: code,
    redirect_uri: redirect_uri || redirect_uri_value,
    client_id: @client_id,
    code_verifier: verifier
  )
  parse_token_response(body)
end

#authorize_url(user:, verifier: nil, state: nil, extra_params: {}) ⇒ Object

Returns the authorization URL to redirect the user to. The code verifier (and state) are persisted via the storage object when one is configured, so the callback can recover them.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ask/auth/providers/oauth.rb', line 66

def authorize_url(user:, verifier: nil, state: nil, extra_params: {})
  verifier ||= generate_code_verifier
  state ||= SecureRandom.hex(16)
  challenge = generate_code_challenge(verifier)

  persist_oauth_state(user, verifier, state)

  params = {
    response_type: "code",
    client_id: @client_id,
    redirect_uri: redirect_uri_value,
    scope: @scope.to_s,
    state: state,
    code_challenge: challenge,
    code_challenge_method: "S256"
  }.merge(extra_params)

  uri = URI.parse(@authorize_url || "https://example.com/oauth/authorize")
  uri.query = URI.encode_www_form(params)
  uri.to_s
end

#call(name, user: nil) ⇒ Object

Returns nil (no automatic resolution) — OAuth requires interactive flow.



46
47
48
# File 'lib/ask/auth/providers/oauth.rb', line 46

def call(name, user: nil)
  nil
end

#generate_code_challenge(verifier) ⇒ Object

Generate a PKCE code challenge (SHA256 base64 digest of verifier).



56
57
58
59
60
61
# File 'lib/ask/auth/providers/oauth.rb', line 56

def generate_code_challenge(verifier)
  ::Base64.urlsafe_encode64(
    OpenSSL::Digest.digest("SHA256", verifier),
    padding: false
  )
end

#generate_code_verifierObject

Generate a PKCE code verifier (128-char alphanumeric string).



51
52
53
# File 'lib/ask/auth/providers/oauth.rb', line 51

def generate_code_verifier
  SecureRandom.alphanumeric(128)
end

#refresh(refresh_token:) ⇒ Object

Refresh an access token using a refresh token grant.



110
111
112
113
114
115
116
117
# File 'lib/ask/auth/providers/oauth.rb', line 110

def refresh(refresh_token:)
  body = token_exchange(
    grant_type: "refresh_token",
    refresh_token: refresh_token,
    client_id: @client_id
  )
  parse_token_response(body)
end