Class: LlmGateway::Clients::ClaudeCode::OAuthFlow

Inherits:
Object
  • Object
show all
Defined in:
lib/llm_gateway/clients/claude_code/oauth_flow.rb

Constant Summary collapse

CLIENT_ID =
"9d1c250a-e61b-44d9-88ed-5944d1962f5e"
TOKEN_URL =
"https://api.anthropic.com/v1/oauth/token"
AUTH_URL =
"https://claude.ai/oauth/authorize"
REDIRECT_URI =
"https://console.anthropic.com/oauth/code/callback"
DEFAULT_SCOPES =
"org:create_api_key user:profile user:inference"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scopes: DEFAULT_SCOPES) ⇒ OAuthFlow

Returns a new instance of OAuthFlow.



23
24
25
26
27
28
29
30
31
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 23

def initialize(
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  scopes: DEFAULT_SCOPES
)
  @client_id = client_id
  @redirect_uri = redirect_uri
  @scopes = scopes
end

Instance Attribute Details

#client_idObject (readonly)

Returns the value of attribute client_id.



21
22
23
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 21

def client_id
  @client_id
end

#redirect_uriObject (readonly)

Returns the value of attribute redirect_uri.



21
22
23
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 21

def redirect_uri
  @redirect_uri
end

#scopesObject (readonly)

Returns the value of attribute scopes.



21
22
23
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 21

def scopes
  @scopes
end

Instance Method Details

#exchange_code(auth_code_or_callback, code_verifier, state: nil) ⇒ Object

Step 2: Exchange the authorization code for tokens. Accepts one of:

  • “code#state” (legacy format)

  • a raw authorization code, with state passed separately

  • a full callback URL containing ?code=…&state=…

Returns { access_token:, refresh_token:, expires_at: }



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 53

def exchange_code(auth_code_or_callback, code_verifier, state: nil)
  code, resolved_state = extract_code_and_state(auth_code_or_callback, state)

  uri = URI(TOKEN_URL)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.read_timeout = 30
  http.open_timeout = 10

  request = Net::HTTP::Post.new(uri)
  request["Content-Type"] = "application/json"

  request.body = {
    grant_type: "authorization_code",
    client_id: @client_id,
    code: code,
    state: resolved_state || "",
    redirect_uri: @redirect_uri,
    code_verifier: code_verifier
  }.to_json

  response = http.request(request)

  if response.code.to_i == 200
    data = JSON.parse(response.body)

    expires_at = if data["expires_in"]
                   Time.now + data["expires_in"].to_i
    elsif data["expires_at"]
                   Time.parse(data["expires_at"])
    end

    {
      access_token: data["access_token"],
      refresh_token: data["refresh_token"],
      expires_at: expires_at
    }
  else
    error_body = begin
      JSON.parse(response.body)
    rescue StandardError
      {}
    end
    raise Errors::AuthenticationError.new(
      "OAuth token exchange failed: #{error_body["error_description"] || error_body["error"] || response.body}",
      error_body["error"]
    )
  end
end

#parse_callback(callback_url) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 103

def parse_callback(callback_url)
  uri = URI(callback_url)
  code = uri.query && URI.decode_www_form(uri.query).to_h["code"]
  state = uri.query && URI.decode_www_form(uri.query).to_h["state"]

  raise ArgumentError, "Callback URL is missing code parameter" if code.nil? || code.empty?

  { code: code, state: state }
rescue URI::InvalidURIError => e
  raise ArgumentError, "Invalid callback URL: #{e.message}"
end

#start(state: SecureRandom.hex(16)) ⇒ Object

Step 1: Generate the authorization URL for the user to visit. Returns a hash with everything needed to complete the flow later.



35
36
37
38
39
40
41
42
43
44
45
# File 'lib/llm_gateway/clients/claude_code/oauth_flow.rb', line 35

def start(state: SecureRandom.hex(16))
  code_verifier, code_challenge = generate_pkce

  auth_url = build_authorization_url(code_challenge, state)

  {
    authorization_url: auth_url,
    code_verifier: code_verifier,
    state: state
  }
end