Class: Kward::OpenAIOAuth
- Inherits:
-
Object
- Object
- Kward::OpenAIOAuth
- Defined in:
- lib/kward/auth/openai_oauth.rb
Constant Summary collapse
- ISSUER =
"https://auth.openai.com"- TOKEN_URL =
URI("#{ISSUER}/oauth/token")
- DEFAULT_PORT =
1455- CALLBACK_PATH =
"/auth/callback"- SCOPE =
"openid profile email offline_access api.connectors.read api.connectors.invoke"- DEFAULT_CLIENT_ID =
"app_EMoamEEZ73f0CkXaXp7hrann"
Instance Attribute Summary collapse
-
#auth_path ⇒ Object
readonly
Returns the value of attribute auth_path.
Class Method Summary collapse
Instance Method Summary collapse
- #access_token ⇒ Object
- #account_id ⇒ Object
- #authorization_code_from(input, expected_state: nil) ⇒ Object
- #authorization_url(redirect_uri:, code_challenge:, state:) ⇒ Object
- #complete_login_flow(code:, redirect_uri:, code_verifier:) ⇒ Object
-
#initialize(auth_path: OpenAIOAuth.default_auth_path, client_id: nil, config_path: OpenAIOAuth.default_config_path, issuer: ISSUER) ⇒ OpenAIOAuth
constructor
A new instance of OpenAIOAuth.
- #logged_in? ⇒ Boolean
- #login(prompt:, open_browser: true, timeout_seconds: 120) ⇒ Object
- #refresh! ⇒ Object
- #save_auth(tokens: {}) ⇒ Object
- #start_login_flow ⇒ Object
- #wait_for_login_callback(server, expected_state:, timeout_seconds:) ⇒ Object
Constructor Details
#initialize(auth_path: OpenAIOAuth.default_auth_path, client_id: nil, config_path: OpenAIOAuth.default_config_path, issuer: ISSUER) ⇒ OpenAIOAuth
Returns a new instance of OpenAIOAuth.
22 23 24 25 26 27 |
# File 'lib/kward/auth/openai_oauth.rb', line 22 def initialize(auth_path: OpenAIOAuth.default_auth_path, client_id: nil, config_path: OpenAIOAuth.default_config_path, issuer: ISSUER) @auth_path = File.(auth_path) @client_id = client_id @config_path = File.(config_path) @issuer = issuer.delete_suffix("/") end |
Instance Attribute Details
#auth_path ⇒ Object (readonly)
Returns the value of attribute auth_path.
20 21 22 |
# File 'lib/kward/auth/openai_oauth.rb', line 20 def auth_path @auth_path end |
Class Method Details
.default_auth_path ⇒ Object
29 30 31 |
# File 'lib/kward/auth/openai_oauth.rb', line 29 def self.default_auth_path File.(ENV["KWARD_AUTH_PATH"] || "~/.kward/auth.json") end |
.default_config_path ⇒ Object
33 34 35 |
# File 'lib/kward/auth/openai_oauth.rb', line 33 def self.default_config_path File.(ENV["KWARD_CONFIG_PATH"] || "~/.kward/config.json") end |
Instance Method Details
#access_token ⇒ Object
37 38 39 40 |
# File 'lib/kward/auth/openai_oauth.rb', line 37 def access_token auth = current_auth auth&.fetch("tokens", {})&.fetch("access_token", nil) end |
#account_id ⇒ Object
42 43 44 45 |
# File 'lib/kward/auth/openai_oauth.rb', line 42 def account_id auth = current_auth auth&.fetch("account_id", nil) || auth&.fetch("tokens", {})&.fetch("account_id", nil) end |
#authorization_code_from(input, expected_state: nil) ⇒ Object
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/kward/auth/openai_oauth.rb', line 116 def (input, expected_state: nil) value = input.strip return "" if value.empty? uri = URI.parse(value) params = URI.decode_www_form(uri.query.to_s).to_h if params.key?("code") raise "OAuth state mismatch" if expected_state && params["state"] != expected_state return params["code"] end value rescue URI::InvalidURIError value end |
#authorization_url(redirect_uri:, code_challenge:, state:) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/kward/auth/openai_oauth.rb', line 76 def (redirect_uri:, code_challenge:, state:) query = URI.encode_www_form( response_type: "code", client_id: client_id, redirect_uri: redirect_uri, scope: SCOPE, code_challenge: code_challenge, code_challenge_method: "S256", id_token_add_organizations: "true", codex_cli_simplified_flow: "true", state: state, originator: "kward" ) "#{@issuer}/oauth/authorize?#{query}" end |
#complete_login_flow(code:, redirect_uri:, code_verifier:) ⇒ Object
110 111 112 113 114 |
# File 'lib/kward/auth/openai_oauth.rb', line 110 def complete_login_flow(code:, redirect_uri:, code_verifier:) tokens = exchange_code_for_tokens(code: code, redirect_uri: redirect_uri, code_verifier: code_verifier) save_auth(tokens: tokens) tokens end |
#logged_in? ⇒ Boolean
47 48 49 |
# File 'lib/kward/auth/openai_oauth.rb', line 47 def logged_in? !access_token.to_s.empty? end |
#login(prompt:, open_browser: true, timeout_seconds: 120) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/kward/auth/openai_oauth.rb', line 51 def login(prompt:, open_browser: true, timeout_seconds: 120) flow = start_login_flow pkce = flow[:pkce] state = flow[:state] server = flow[:server] redirect_uri = flow[:redirect_uri] url = flow[:authorization_url] prompt.say("OpenAI login URL:\n#{url}\n") prompt.say("Waiting for browser login. If it does not complete, paste the callback URL when prompted.") browser_opened = open_browser && open_url(url) code = wait_for_callback(server, expected_state: state, timeout_seconds: browser_opened ? timeout_seconds : 5) unless code input = prompt.ask("Paste callback URL or authorization code:") code = (input.to_s, expected_state: state) end raise "Missing authorization code" if code.to_s.empty? complete_login_flow(code: code, redirect_uri: redirect_uri, code_verifier: pkce[:verifier]) auth_path ensure server&.close unless server&.closed? end |
#refresh! ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/kward/auth/openai_oauth.rb', line 146 def refresh! auth = load_auth || raise("OpenAI OAuth login not found") refresh_token = auth.fetch("tokens", {}).fetch("refresh_token", nil) raise "OpenAI OAuth refresh token not found" if refresh_token.to_s.empty? response = post_json(TOKEN_URL, client_id: client_id, grant_type: "refresh_token", refresh_token: refresh_token) refreshed = parse_successful_json(response, "OpenAI OAuth token refresh") save_auth(tokens: (auth.fetch("tokens", {}) || {}).merge(refreshed)) load_auth end |
#save_auth(tokens: {}) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/kward/auth/openai_oauth.rb', line 133 def save_auth(tokens: {}) account_id = extract_account_id(tokens) data = { "auth_mode" => "openai_oauth", "tokens" => account_id ? tokens.merge("account_id" => account_id) : tokens, "account_id" => account_id, "saved_at" => Time.now.utc.iso8601, "expires_at" => expires_at_for(tokens) }.compact AuthFile.write_json(@auth_path, data) end |
#start_login_flow ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/kward/auth/openai_oauth.rb', line 92 def start_login_flow pkce = generate_pkce state = random_urlsafe(32) server = start_callback_server redirect_uri = "http://localhost:#{server.addr[1]}#{CALLBACK_PATH}" { pkce: pkce, state: state, server: server, redirect_uri: redirect_uri, authorization_url: (redirect_uri: redirect_uri, code_challenge: pkce[:challenge], state: state) } end |
#wait_for_login_callback(server, expected_state:, timeout_seconds:) ⇒ Object
106 107 108 |
# File 'lib/kward/auth/openai_oauth.rb', line 106 def wait_for_login_callback(server, expected_state:, timeout_seconds:) wait_for_callback(server, expected_state: expected_state, timeout_seconds: timeout_seconds) end |