Class: Ask::Auth::Providers::OpenaiCodex
- 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.(user: current_user)
tokens = provider.(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
Class Method Summary collapse
-
.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.
-
.allowed_model?(model_id) ⇒ Boolean
Whether a model id is usable through a ChatGPT subscription's OAuth path.
- .decode_jwt_claims(token) ⇒ Object
Instance Method Summary collapse
- #authorize_url(user:, verifier: nil, state: nil) ⇒ Object
-
#initialize(storage: nil, client_id: CLIENT_ID, redirect_uri: nil, http: Ask::Auth::OAuth::HTTP) ⇒ OpenaiCodex
constructor
A new instance of OpenaiCodex.
- #parse_token_response(body) ⇒ Object
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.account_id_from(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.
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 (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.account_id_from(data["id_token"]), raw: data } rescue JSON::ParserError => e raise OAuthError, "bad token response: #{e.}" end |