Class: LlmGateway::Clients::OpenAI::OAuthFlow
- Inherits:
-
Object
- Object
- LlmGateway::Clients::OpenAI::OAuthFlow
- Defined in:
- lib/llm_gateway/clients/openai_codex/oauth_flow.rb
Constant Summary collapse
- CLIENT_ID =
"app_EMoamEEZ73f0CkXaXp7hrann"- AUTHORIZE_URL =
"https://auth.openai.com/oauth/authorize"- TOKEN_URL =
"https://auth.openai.com/oauth/token"- REDIRECT_URI =
"http://localhost:1455/auth/callback"- SCOPE =
"openid profile email offline_access"- JWT_CLAIM_PATH =
"https://api.openai.com/auth"
Instance Attribute Summary collapse
-
#client_id ⇒ Object
readonly
Returns the value of attribute client_id.
-
#redirect_uri ⇒ Object
readonly
Returns the value of attribute redirect_uri.
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
Class Method Summary collapse
-
.extract_account_id_from_token(token) ⇒ Object
Extract the ChatGPT account_id from a JWT access token.
-
.refresh_access_token(refresh_token, client_id: CLIENT_ID) ⇒ Object
Refresh an existing access token (class method).
Instance Method Summary collapse
-
#exchange_code(input, code_verifier, expected_state: nil) ⇒ Object
Step 2: Exchange the authorization code for tokens.
-
#initialize(client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scope: SCOPE) ⇒ OAuthFlow
constructor
A new instance of OAuthFlow.
-
#login ⇒ Object
Interactive OAuth flow: print URL, wait for paste, return tokens.
-
#parse_callback(callback_url) ⇒ Object
Parse a callback URL (or query string) into { code:, state: }.
-
#start(state: SecureRandom.hex(16)) ⇒ Object
Step 1: Generate the authorization URL and PKCE values.
Constructor Details
#initialize(client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scope: SCOPE) ⇒ OAuthFlow
Returns a new instance of OAuthFlow.
24 25 26 27 28 29 30 31 32 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 24 def initialize( client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scope: SCOPE ) @client_id = client_id @redirect_uri = redirect_uri @scope = scope end |
Instance Attribute Details
#client_id ⇒ Object (readonly)
Returns the value of attribute client_id.
22 23 24 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 22 def client_id @client_id end |
#redirect_uri ⇒ Object (readonly)
Returns the value of attribute redirect_uri.
22 23 24 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 22 def redirect_uri @redirect_uri end |
#scope ⇒ Object (readonly)
Returns the value of attribute scope.
22 23 24 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 22 def scope @scope end |
Class Method Details
.extract_account_id_from_token(token) ⇒ Object
Extract the ChatGPT account_id from a JWT access token.
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 174 def self.extract_account_id_from_token(token) parts = token.to_s.split(".") return nil unless parts.length == 3 payload_b64 = parts[1] # Re-pad to a multiple of 4 for base64 decoding payload_b64 += "=" * ((4 - payload_b64.length % 4) % 4) payload = JSON.parse(Base64.urlsafe_decode64(payload_b64)) auth = payload[JWT_CLAIM_PATH] account_id = auth&.dig("chatgpt_account_id") account_id.is_a?(String) && !account_id.empty? ? account_id : nil rescue StandardError nil end |
.refresh_access_token(refresh_token, client_id: CLIENT_ID) ⇒ Object
Refresh an existing access token (class method). Returns { access_token:, refresh_token:, expires_at:, account_id: }
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 130 def self.refresh_access_token(refresh_token, client_id: CLIENT_ID) 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/x-www-form-urlencoded" request.body = URI.encode_www_form( grant_type: "refresh_token", refresh_token: refresh_token, client_id: client_id ) response = http.request(request) if response.code.to_i == 200 data = JSON.parse(response.body) unless data["access_token"] && data["refresh_token"] && data["expires_in"] raise "Token refresh response missing required fields" end expires_at = Time.now + data["expires_in"].to_i account_id = extract_account_id_from_token(data["access_token"]) { access_token: data["access_token"], refresh_token: data["refresh_token"], expires_at: expires_at, account_id: account_id } else error_body = begin JSON.parse(response.body) rescue StandardError {} end raise "Token refresh failed (#{response.code}): #{error_body["error_description"] || error_body["error"] || response.body}" end end |
Instance Method Details
#exchange_code(input, code_verifier, expected_state: nil) ⇒ Object
Step 2: Exchange the authorization code for tokens. Accepts a raw code string, a full redirect URL, or code#state format. Returns { access_token:, refresh_token:, expires_at:, account_id: }
49 50 51 52 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 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 49 def exchange_code(input, code_verifier, expected_state: nil) code = (input, expected_state) raise ArgumentError, "Missing authorization code" unless code 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/x-www-form-urlencoded" request.body = URI.encode_www_form( grant_type: "authorization_code", client_id: @client_id, code: code, code_verifier: code_verifier, redirect_uri: @redirect_uri ) response = http.request(request) if response.code.to_i == 200 data = JSON.parse(response.body) unless data["access_token"] && data["refresh_token"] && data["expires_in"] raise "Token response missing required fields: #{data.keys.join(", ")}" end expires_at = Time.now + data["expires_in"].to_i account_id = self.class.extract_account_id_from_token(data["access_token"]) raise "Failed to extract account_id from access token" unless account_id { access_token: data["access_token"], refresh_token: data["refresh_token"], expires_at: expires_at, account_id: account_id } else error_body = parse_error_body(response.body) raise "OAuth token exchange failed (#{response.code}): #{error_body["error_description"] || error_body["error"] || response.body}" end end |
#login ⇒ Object
Interactive OAuth flow: print URL, wait for paste, return tokens. Returns { access_token:, refresh_token:, expires_at:, account_id: }
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 108 def login flow = start puts "Open this URL to authorize with OpenAI:" puts flow[:authorization_url] puts puts "After logging in your browser will redirect to localhost (the page won't load)." puts "Copy the full URL from your browser's address bar and paste it below." puts print "Paste the redirect URL (or authorization code): " tty = File.open("/dev/tty", "r") input = tty.gets&.strip tty.close raise "No authorization code provided" if input.nil? || input.empty? exchange_code(input, flow[:code_verifier], expected_state: flow[:state]) end |
#parse_callback(callback_url) ⇒ Object
Parse a callback URL (or query string) into { code:, state: }
95 96 97 98 99 100 101 102 103 104 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 95 def parse_callback(callback_url) uri = URI.parse(callback_url) params = URI.decode_www_form(uri.query.to_s).to_h code = params["code"] raise ArgumentError, "Callback URL is missing code parameter" if code.nil? || code.empty? { code: code, state: params["state"] } rescue URI::InvalidURIError => e raise ArgumentError, "Invalid callback URL: #{e.}" end |
#start(state: SecureRandom.hex(16)) ⇒ Object
Step 1: Generate the authorization URL and PKCE values. Returns { authorization_url:, code_verifier:, state: }
36 37 38 39 40 41 42 43 44 |
# File 'lib/llm_gateway/clients/openai_codex/oauth_flow.rb', line 36 def start(state: SecureRandom.hex(16)) code_verifier, code_challenge = generate_pkce { authorization_url: (code_challenge, state), code_verifier: code_verifier, state: state } end |