rig_core/providers/chatgpt/auth/mod.rs
1//! ChatGPT access-token configuration and native OAuth authentication.
2//!
3//! ```no_run
4//! use rig_core::providers::chatgpt::auth::{AuthSource, Authenticator, DeviceCodeHandler, FileTokenStore};
5//!
6//! let store = FileTokenStore::new("/path/to/credentials");
7//! let auth = Authenticator::new(AuthSource::OAuth, store, DeviceCodeHandler::default(), true);
8//! ```
9
10use crate::http_client::HttpClientExt;
11use std::fmt;
12
13#[cfg(not(target_family = "wasm"))]
14pub use crate::providers::internal::auth::FileTokenStore;
15pub use crate::providers::internal::auth::{
16 DeviceCodeHandler, DeviceCodePrompt, MemoryTokenStore, StoreLock, TokenStore,
17};
18
19#[cfg(not(target_family = "wasm"))]
20mod native;
21
22/// The key the sign-in is kept under in the store.
23#[cfg(not(target_family = "wasm"))]
24const STORE_KEY: &str = super::PROVIDER_NAME;
25
26/// What a browser sign-in shows the user while it waits.
27#[derive(Debug, Clone)]
28pub struct BrowserSignInPrompt {
29 /// The sign-in page, for the user to open when the browser did not.
30 pub authorize_url: String,
31 /// Whether the desktop's opener started; the page may still not show.
32 pub browser_launched: bool,
33}
34
35/// How a sign-in asks the user to authorize: in the browser, or with a
36/// code entered on another device.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum SignInMethod {
39 /// A page opened in the browser, returning to a local listener.
40 Browser,
41 /// A code the user enters at a URL, on any device.
42 DeviceCode,
43}
44
45/// What [`Authenticator::sign_in`] asks the user to do, besides entering a
46/// device code, which goes to the authenticator's [`DeviceCodeHandler`].
47#[derive(Debug, Clone)]
48pub enum SignInPrompt {
49 /// Sign in on the page the browser was asked to open.
50 Browser(BrowserSignInPrompt),
51 /// The browser sign-in could not listen for its callback, for this
52 /// reason; a device code follows.
53 BrowserUnavailable(String),
54}
55
56/// What [`Authenticator::sign_out`] did.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum SignedOut {
59 /// No sign-in was kept.
60 NotSignedIn,
61 /// The sign-in was deleted from the store and forgotten. `revoked` says
62 /// whether the provider revoked its tokens too, or why not.
63 Forgotten {
64 /// The revocation, or why it failed.
65 revoked: Result<(), String>,
66 },
67}
68
69/// Where the ChatGPT credential comes from: a given access token, or a stored OAuth sign-in.
70#[derive(Clone)]
71pub enum AuthSource {
72 /// A token the caller already holds.
73 AccessToken {
74 /// The token.
75 access_token: String,
76 /// The ChatGPT account it belongs to, when known.
77 account_id: Option<String>,
78 },
79 /// The OAuth sign-in kept in the token store, refreshed as needed.
80 OAuth,
81}
82
83impl fmt::Debug for AuthSource {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 Self::AccessToken { .. } => f.write_str("AccessToken(<redacted>)"),
87 Self::OAuth => f.write_str("OAuth"),
88 }
89 }
90}
91
92/// Signs in to ChatGPT and keeps the stored credential fresh. Clones share
93/// the store and the refresh lock.
94#[derive(Clone)]
95pub struct Authenticator(Shell<AuthSource>);
96
97impl fmt::Debug for Authenticator {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 self.0.fmt(f)
100 }
101}
102
103use crate::providers::internal::auth::Shell;
104pub use crate::providers::internal::auth::{AuthContext, AuthError};
105
106impl Authenticator {
107 /// An authenticator over `source`, keeping an OAuth sign-in in `store`
108 /// under [`PROVIDER_NAME`](super::PROVIDER_NAME), `chatgpt`;
109 /// `device_code_handler` shows a device code when the device flow is
110 /// allowed.
111 pub fn new(
112 source: AuthSource,
113 store: impl TokenStore + 'static,
114 device_code_handler: DeviceCodeHandler,
115 allow_device_flow: bool,
116 ) -> Self {
117 Self(Shell::new(
118 source,
119 store,
120 device_code_handler,
121 allow_device_flow,
122 ))
123 }
124
125 /// Forget the credential held in memory, so the next request reads the
126 /// store: after the store's record is deleted, as a sign-out does.
127 pub fn forget(&self) {
128 self.0.remembered.forget();
129 }
130
131 /// This authenticator showing device codes with `handler`, still
132 /// sharing its store and refresh lock with the original.
133 #[must_use]
134 pub fn with_device_code_handler(mut self, handler: DeviceCodeHandler) -> Self {
135 self.0.device_code_handler = handler;
136 self
137 }
138
139 /// Resolve the access token and account id, refreshing through `http` as needed.
140 /// Return cache, transport, or authorization errors. OAuth is unsupported
141 /// on WASM; explicit access tokens remain available.
142 #[cfg_attr(target_family = "wasm", allow(unused_variables))]
143 pub async fn auth_context<H>(&self, http: &H) -> Result<AuthContext, AuthError>
144 where
145 H: HttpClientExt,
146 {
147 match &self.0.source {
148 AuthSource::AccessToken {
149 access_token,
150 account_id,
151 } => Ok(AuthContext {
152 api_key: access_token.clone().into(),
153 account_id: account_id.clone(),
154 api_base: None,
155 }),
156 #[cfg(not(target_family = "wasm"))]
157 AuthSource::OAuth => self.auth_context_oauth(http).await,
158 #[cfg(target_family = "wasm")]
159 AuthSource::OAuth => Err(AuthError::Message(
160 "ChatGPT OAuth is not supported on wasm targets".into(),
161 )),
162 }
163 }
164}