Class: Ask::Auth::Providers::OpenaiCodex

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

Overview

OpenAI Codex OAuth — "bring your ChatGPT subscription" (Plus/Pro).

Users authenticate with their OpenAI account (PKCE against auth.openai.com, the public Codex client id) and get tokens that route requests through their subscription quota instead of the pay-per-token API — flat-rate, like Cline/Hermes/opencode.

provider = Ask::Auth::Providers::OpenaiCodex.new(redirect_uri: "...")
url = provider.authorize_url(user: current_user)
tokens = provider.authorize!(user: current_user, code: params[:code])
provider.refresh(refresh_token: tokens[:refresh_token])

#authorize! returns { token:, refresh_token:, expires_at:, account_id:, raw: }. Models are tier-filtered with .allowed_model? (mirrors opencode's codex.ts: the OAuth path excludes pro-reasoning and gpt-5.6, and only gpt-5.4+ general models are allowed).

Constant Summary collapse

CLIENT_ID =
"app_EMoamEEZ73f0CkXaXp7hrann"
ISSUER =
"https://auth.openai.com"
AUTHORIZE_URL =
"#{ISSUER}/oauth/authorize"
TOKEN_URL =
"#{ISSUER}/oauth/token"
SCOPE =
"openid profile email offline_access"
ALLOWED_MODELS =
%w[gpt-5.5 gpt-5.3-codex-spark gpt-5.4 gpt-5.4-mini].freeze
DISALLOWED_MODELS =
%w[gpt-5.5-pro].freeze

Instance Attribute Summary

Attributes inherited from OAuth

#client_id, #redirect_uri

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from OAuth

#authorize!, #call, #generate_code_challenge, #generate_code_verifier, #refresh

Constructor Details

#initialize(storage: nil, client_id: CLIENT_ID, redirect_uri: nil, http: Ask::Auth::OAuth::HTTP) ⇒ OpenaiCodex

Returns a new instance of OpenaiCodex.



36
37
38
39
40
41
42
43
44
45
46
# File 'lib/ask/auth/providers/openai_codex.rb', line 36

def initialize(storage: nil, client_id: CLIENT_ID, redirect_uri: nil, http: Ask::Auth::OAuth::HTTP)
  super(
    storage: storage,
    client_id: client_id,
    authorize_url: AUTHORIZE_URL,
    token_url: TOKEN_URL,
    redirect_uri: redirect_uri,
    scope: SCOPE,
    http: http
  )
end

Class Method Details

.account_id_from(id_token) ⇒ Object

The ChatGPT account id, from the id_token's JWT claims — sent as the ChatGPT-Account-Id header on API calls.



78
79
80
81
82
83
# File 'lib/ask/auth/providers/openai_codex.rb', line 78

def self.(id_token)
  claims = decode_jwt_claims(id_token)
  claims["chatgpt_account_id"] ||
    claims.dig("https://api.openai.com/auth", "chatgpt_account_id") ||
    claims.dig("organizations", 0, "id")
end

.allowed_model?(model_id) ⇒ Boolean

Whether a model id is usable through a ChatGPT subscription's OAuth path. General gpt-5.4+ models and the codex family are allowed; pro-reasoning models (gpt-5.5-pro) and gpt-5.6 are not.

Returns:

  • (Boolean)


68
69
70
71
72
73
74
# File 'lib/ask/auth/providers/openai_codex.rb', line 68

def self.allowed_model?(model_id)
  return true if ALLOWED_MODELS.include?(model_id)
  return false if DISALLOWED_MODELS.include?(model_id) || model_id == "gpt-5.6"

  match = model_id.match(/\Agpt-(\d+\.\d+)/)
  match ? match[1].to_f > 5.4 : false
end

.decode_jwt_claims(token) ⇒ Object



85
86
87
88
89
90
91
92
93
# File 'lib/ask/auth/providers/openai_codex.rb', line 85

def self.decode_jwt_claims(token)
  payload = token.to_s.split(".")[1]
  return {} if payload.to_s.empty?

  decoded = Base64.urlsafe_decode64(payload)
  JSON.parse(decoded)
rescue ArgumentError, JSON::ParserError
  {}
end

Instance Method Details

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



48
49
50
# File 'lib/ask/auth/providers/openai_codex.rb', line 48

def authorize_url(user:, verifier: nil, state: nil)
  super(user: user, verifier: verifier, state: state, extra_params: {id_token_add_organizations: "true"})
end

#parse_token_response(body) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ask/auth/providers/openai_codex.rb', line 52

def parse_token_response(body)
  data = JSON.parse(body)
  {
    token: data["access_token"],
    refresh_token: data["refresh_token"],
    expires_at: data["expires_in"] ? Time.now + data["expires_in"].to_i : nil,
    account_id: self.class.(data["id_token"]),
    raw: data
  }
rescue JSON::ParserError => e
  raise OAuthError, "bad token response: #{e.message}"
end