Skip to main content

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}