Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oauth): implement authentication flow with login and logout #442

Merged
merged 42 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a68a5fd
feat(oauth): implement OAuth authentication flow with login and logou…
amitksingh1490 Mar 4, 2025
d40a03d
fix: add missing commas and format code for better readability
amitksingh1490 Mar 4, 2025
3269c3b
feat(auth): implement OAuth authentication service with login and log…
amitksingh1490 Mar 4, 2025
7481c31
Merge remote-tracking branch 'origin/main' into feat/integrate-auth
amitksingh1490 Mar 4, 2025
2dbd0eb
chore: update dependencies in Cargo.lock
amitksingh1490 Mar 4, 2025
5570f6b
chore: update dependencies in Cargo.lock to remove unused packages an…
amitksingh1490 Mar 4, 2025
dba4b16
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 4, 2025
928a12d
chore: remove unused forge_oauth dependency from Cargo.toml and Cargo…
amitksingh1490 Mar 4, 2025
ff4c259
chore: remove unused test module for keyring functionality in oauth.rs
amitksingh1490 Mar 4, 2025
9f1e7cb
chore: add MockAuthService to test infrastructure in attachment.rs
amitksingh1490 Mar 4, 2025
863060e
chore: update reqwest dependency to version 0.12.12 and enable rustls…
amitksingh1490 Mar 4, 2025
0871b1f
Merge branch 'main' into feat/integrate-auth
amitksingh1490 Mar 5, 2025
535c0cc
feat: implement unimplemented methods in AuthService and add get_auth…
amitksingh1490 Mar 5, 2025
49e9ad5
feat: update get_auth_token to return Option<String> and add support …
amitksingh1490 Mar 5, 2025
59ef2e0
Merge branch 'main' into feat/integrate-auth
amitksingh1490 Mar 5, 2025
23b39f6
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 5, 2025
ddbc64d
fix(provider): simplify condition check for 'or' in build_provider_se…
amitksingh1490 Mar 5, 2025
4c9dff9
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 5, 2025
27f8f17
Merge remote-tracking branch 'origin/main' into feat/integrate-auth
amitksingh1490 Mar 6, 2025
a9e41bf
refactor(auth): improve error handling and remove unnecessary print s…
amitksingh1490 Mar 6, 2025
743d05a
feat(oauth): integrate forge_oauth crate and update authentication flow
amitksingh1490 Mar 6, 2025
c379e75
feat(auth): update ClerkConfig initialization and add key_url paramet…
amitksingh1490 Mar 6, 2025
d5bbe13
refactor(auth): format code for better readability in ForgeAuthServic…
amitksingh1490 Mar 6, 2025
934864e
refactor(ui): remove unnecessary log statements from authentication flow
amitksingh1490 Mar 6, 2025
4423721
fix(provider): correct base URL for Antinomy provider
amitksingh1490 Mar 6, 2025
63cd373
refactor(api): simplify imports by using wildcard for forge_domain
amitksingh1490 Mar 6, 2025
c025f1f
refactor(auth): rename authentication methods for clarity and consist…
tusharmath Mar 6, 2025
37159f9
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 6, 2025
6d2d6cc
refactor: add mutex in ForgeProviderService
tusharmath Mar 6, 2025
e888edd
refactor(auth): rename auth_service to credentials_service for clarity
tusharmath Mar 6, 2025
4a133ed
refactor(auth): rename AuthService to CredentialRepository and align …
tusharmath Mar 6, 2025
671cc1f
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 6, 2025
e3a79bc
refactor(provider): replace ProviderBuilder with ClientBuilder and re…
tusharmath Mar 6, 2025
8cbb5d9
refactor(api): simplify header construction by consolidating API key …
amitksingh1490 Mar 6, 2025
377065f
refactor(api): enhance models method to handle multiple provider resp…
amitksingh1490 Mar 6, 2025
b44bc33
refactor(provider): reorganize provider structure and authentication …
tusharmath Mar 6, 2025
d6cffd5
refactor(api): update provider match to use OpenAiCompat for Antinomy
amitksingh1490 Mar 6, 2025
3d61cfa
refactor(provider): update OpenRouter URL to new endpoint
amitksingh1490 Mar 6, 2025
6e96822
refactor(provider): fix Antinomy URL formatting and update environmen…
amitksingh1490 Mar 6, 2025
05e1bb1
refactor(auth): update Clerk OAuth configuration with new URLs and cl…
amitksingh1490 Mar 6, 2025
bea131b
feat(html): add new HTML templates for error handling and success mes…
tusharmath Mar 7, 2025
97fe923
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,443 changes: 1,394 additions & 49 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/forge_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ forge_stream = { path = "../forge_stream" }
forge_app = { path = "../forge_app" }
forge_walker = { path = "../forge_walker" }
forge_infra = { path = "../forge_infra" }
forge_oauth = { path = "../forge_oauth" }
serde_yaml = "0.9.34"
serde_json = { version = "1.0" }

Expand Down
19 changes: 18 additions & 1 deletion crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::path::Path;
use std::sync::Arc;

use anyhow::Result;
use forge_app::{EnvironmentService, ForgeApp, Infrastructure};
use forge_app::{AuthService, EnvironmentService, ForgeApp, Infrastructure};
use forge_domain::*;
use forge_infra::ForgeInfra;
use forge_oauth::AuthFlowState;
use forge_stream::MpscStream;
use serde_json::Value;

Expand Down Expand Up @@ -64,6 +65,22 @@ impl<F: App + Infrastructure> API for ForgeAPI<F> {
self.app.conversation_service().create(workflow).await
}

fn auth_url(&self) -> AuthFlowState {
self.app.auth_service().auth_url()
}

async fn authenticate(&self, auth_flow_state: AuthFlowState) -> anyhow::Result<()> {
self.app.auth_service().authenticate(auth_flow_state).await
}

fn logout(&self) -> anyhow::Result<bool> {
self.app.auth_service().logout()
}

fn get_key(&self) -> Option<String> {
self.app.auth_service().get_auth_token()
}

fn environment(&self) -> Environment {
self.app.environment_service().get_environment().clone()
}
Expand Down
12 changes: 12 additions & 0 deletions crates/forge_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::path::Path;

pub use api::*;
pub use forge_domain::*;
use forge_oauth::AuthFlowState;
use forge_stream::MpscStream;
use serde_json::Value;

Expand All @@ -29,6 +30,17 @@ pub trait API {
chat: ChatRequest,
) -> anyhow::Result<MpscStream<anyhow::Result<AgentMessage<ChatResponse>, anyhow::Error>>>;

fn auth_url(&self) -> AuthFlowState;
/// Authenticates the user with Clerk OAuth
async fn authenticate(&self, auth_flow_state: AuthFlowState) -> anyhow::Result<()>;

/// Logs out the user by deleting stored credentials
/// Returns true if credentials were found and deleted, false otherwise
fn logout(&self) -> anyhow::Result<bool>;

/// Returns the current authentication token if available
fn get_key(&self) -> Option<String>;

/// Returns the current environment
fn environment(&self) -> Environment;

Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ forge_open_router = { path = "../forge_open_router" }
forge_tool_macros = { path = "../forge_tool_macros" }
forge_display = { path = "../forge_display" }
forge_walker = { path = "../forge_walker" }
forge_oauth = { path = "../forge_oauth" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.133"
derive_setters = "0.1.6"
Expand Down
9 changes: 7 additions & 2 deletions crates/forge_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::Infrastructure;
pub struct ForgeApp<F> {
infra: Arc<F>,
tool_service: Arc<ForgeToolService>,
provider_service: ForgeProviderService,
provider_service: ForgeProviderService<F>,
conversation_service: ForgeConversationService,
prompt_service: ForgeTemplateService<F, ForgeToolService>,
attachment_service: ForgeChatRequest<F>,
Expand All @@ -40,7 +40,7 @@ impl<F: Infrastructure> ForgeApp<F> {

impl<F: Infrastructure> App for ForgeApp<F> {
type ToolService = ForgeToolService;
type ProviderService = ForgeProviderService;
type ProviderService = ForgeProviderService<F>;
type ConversationService = ForgeConversationService;
type TemplateService = ForgeTemplateService<F, ForgeToolService>;
type AttachmentService = ForgeChatRequest<F>;
Expand All @@ -67,11 +67,16 @@ impl<F: Infrastructure> App for ForgeApp<F> {
}

impl<F: Infrastructure> Infrastructure for ForgeApp<F> {
type AuthService = F::AuthService;
type EnvironmentService = F::EnvironmentService;
type FileReadService = F::FileReadService;
type VectorIndex = F::VectorIndex;
type EmbeddingService = F::EmbeddingService;

fn auth_service(&self) -> &Self::AuthService {
self.infra.auth_service()
}

fn environment_service(&self) -> &Self::EnvironmentService {
self.infra.environment_service()
}
Expand Down
34 changes: 32 additions & 2 deletions crates/forge_app/src/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,20 @@ impl<F: Infrastructure> AttachmentService for ForgeChatRequest<F> {

#[cfg(test)]
mod tests {
use core::str;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use base64::Engine;
use bytes::Bytes;
use forge_domain::{AttachmentService, ContentType, Environment, Point, Query, Suggestion};
use forge_oauth::AuthFlowState;

use crate::attachment::ForgeChatRequest;
use crate::{
EmbeddingService, EnvironmentService, FileReadService, Infrastructure, VectorIndex,
AuthService, EmbeddingService, EnvironmentService, FileReadService, Infrastructure,
VectorIndex,
};

struct MockEnvironmentService {}
Expand All @@ -90,6 +93,7 @@ mod tests {
provider_key: "key".to_string(),
provider_url: "url".to_string(),
openai_key: None,
force_antinomy: None,
}
}
}
Expand Down Expand Up @@ -162,6 +166,7 @@ mod tests {
file_service: MockFileReadService,
vector_index: MockVectorIndex,
embedding_service: MockEmbeddingService,
auth_service: MockAuthService,
}

impl MockInfrastructure {
Expand All @@ -171,16 +176,37 @@ mod tests {
file_service: MockFileReadService::new(),
vector_index: MockVectorIndex {},
embedding_service: MockEmbeddingService {},
auth_service: MockAuthService {},
}
}
}

struct MockAuthService {}

#[async_trait::async_trait]
impl AuthService for MockAuthService {
fn auth_url(&self) -> AuthFlowState {
unimplemented!()
}
async fn authenticate(&self, _: AuthFlowState) -> Result<(), anyhow::Error> {
Ok(())
}

fn logout(&self) -> Result<bool, anyhow::Error> {
Ok(false)
}

fn get_auth_token(&self) -> Option<String> {
None
}
}

impl Infrastructure for MockInfrastructure {
type EnvironmentService = MockEnvironmentService;
type FileReadService = MockFileReadService;
type VectorIndex = MockVectorIndex;
type EmbeddingService = MockEmbeddingService;

type AuthService = MockAuthService;
fn environment_service(&self) -> &Self::EnvironmentService {
&self.env_service
}
Expand All @@ -196,6 +222,10 @@ mod tests {
fn embedding_service(&self) -> &Self::EmbeddingService {
&self.embedding_service
}

fn auth_service(&self) -> &Self::AuthService {
&self.auth_service
}
}

#[tokio::test]
Expand Down
19 changes: 19 additions & 0 deletions crates/forge_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ use std::path::Path;
pub use app::*;
use bytes::Bytes;
use forge_domain::{Point, Query, Suggestion};
use forge_oauth::AuthFlowState;

#[async_trait::async_trait]
pub trait AuthService: Send + Sync + 'static {
/// Returns the current authentication state
fn auth_url(&self) -> AuthFlowState;
/// Authenticates the user and stores credentials
async fn authenticate(&self, auth_flow_state: AuthFlowState) -> anyhow::Result<()>;

/// Logs out the user by removing stored credentials
/// Returns true if credentials were found and removed, false otherwise
fn logout(&self) -> anyhow::Result<bool>;

/// Retrieves the current authentication token if available
/// Returns the token as a string if found, or an error if not authenticated
fn get_auth_token(&self) -> Option<String>;
}

/// Repository for accessing system environment information
#[async_trait::async_trait]
Expand Down Expand Up @@ -45,11 +62,13 @@ pub trait EmbeddingService: Send + Sync {
}

pub trait Infrastructure: Send + Sync + 'static {
type AuthService: AuthService;
type EnvironmentService: EnvironmentService;
type FileReadService: FileReadService;
type VectorIndex: VectorIndex<Suggestion>;
type EmbeddingService: EmbeddingService;

fn auth_service(&self) -> &Self::AuthService;
fn environment_service(&self) -> &Self::EnvironmentService;
fn file_read_service(&self) -> &Self::FileReadService;
fn vector_index(&self) -> &Self::VectorIndex;
Expand Down
70 changes: 54 additions & 16 deletions crates/forge_app/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,85 @@ use forge_domain::{
use forge_open_router::ProviderBuilder;
use moka2::future::Cache;

use crate::{EnvironmentService, Infrastructure};
use crate::{AuthService, EnvironmentService, Infrastructure};

pub struct ForgeProviderService {
or: Box<dyn ProviderService>,
pub struct ForgeProviderService<F> {
infra: Arc<F>,
or: Option<Arc<dyn ProviderService>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
or: Option<Arc<dyn ProviderService>>,
or: Option<Arc<P>>,

Don't use dyn.

cache: Cache<ModelId, Parameters>,
}

impl ForgeProviderService {
pub fn new<F: Infrastructure>(infra: Arc<F>) -> Self {
let env = infra.environment_service().get_environment();
let or = ProviderBuilder::from_url(env.provider_url)
.with_key(env.provider_key)
.build()
.expect("Failed to build provider");
impl<F: Infrastructure> ForgeProviderService<F> {
pub fn new(infra: Arc<F>) -> Self {
let infra = infra.clone();
Self { infra, or: None, cache: Cache::new(1024) }
}

fn with_provider_service(&self, provider: Arc<dyn ProviderService>) -> Self {
Self {
infra: self.infra.clone(),
or: Some(provider),
cache: self.cache.clone(),
}
}

Self { or, cache: Cache::new(1024) }
fn build_provider_service(&self) -> Result<Self> {
if self.or.is_some() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will never be some.

return Ok(Self {
infra: self.infra.clone(),
or: self.or.clone(),
cache: self.cache.clone(),
});
}
let env = self.infra.environment_service().get_environment();
let key = if let Some(_antinomy) = env.force_antinomy {
self.infra
.auth_service()
.get_auth_token()
.ok_or_else(|| anyhow::anyhow!("No auth token found run login command to login"))?
} else {
env.provider_key.clone()
};
let or = ProviderBuilder::from_url(env.provider_url)
.with_key(key)
.build()?;
Ok(self.with_provider_service(or))
}
}

#[async_trait::async_trait]
impl ProviderService for ForgeProviderService {
impl<F: Infrastructure> ProviderService for ForgeProviderService<F> {
async fn chat(
&self,
model_id: &ModelId,
request: ChatContext,
) -> ResultStream<ChatCompletionMessage, anyhow::Error> {
self.or
let this: ForgeProviderService<F> = self.build_provider_service()?;
this.or
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Provider service not initialized"))?
.chat(model_id, request)
.await
.with_context(|| format!("Failed to chat with model: {}", model_id))
}

async fn models(&self) -> Result<Vec<Model>> {
self.or.models().await
let this = self.build_provider_service()?;
this.or
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Provider service not initialized"))?
.models()
.await
}

async fn parameters(&self, model: &ModelId) -> anyhow::Result<Parameters> {
Ok(self
let this = self.build_provider_service()?;
Ok(this
.cache
.try_get_with_by_ref(model, async {
self.or
this.or
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Provider service not initialized"))?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate error messages. Move them to a common place.

.parameters(model)
.await
.with_context(|| format!("Failed to get parameters for model: {}", model))
Expand Down
27 changes: 26 additions & 1 deletion crates/forge_app/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ mod tests {

use bytes::Bytes;
use forge_domain::{Environment, Point, Query, Suggestion};
use forge_oauth::AuthFlowState;

use super::*;
use crate::{EmbeddingService, FileReadService, VectorIndex};
use crate::{AuthService, EmbeddingService, FileReadService, VectorIndex};

/// Create a default test environment
fn stub() -> Stub {
Expand All @@ -64,6 +65,7 @@ mod tests {
provider_url: Default::default(),
provider_key: Default::default(),
openai_key: Default::default(),
force_antinomy: None,
},
}
}
Expand Down Expand Up @@ -102,13 +104,36 @@ mod tests {
}
}

#[async_trait::async_trait]
impl AuthService for Stub {
fn auth_url(&self) -> AuthFlowState {
unimplemented!()
}
async fn authenticate(&self, _auth_flow_state: AuthFlowState) -> anyhow::Result<()> {
unimplemented!()
}

fn logout(&self) -> anyhow::Result<bool> {
unimplemented!()
}

fn get_auth_token(&self) -> Option<String> {
unimplemented!()
}
}

#[async_trait::async_trait]
impl Infrastructure for Stub {
type AuthService = Stub;
type EnvironmentService = Stub;
type FileReadService = Stub;
type VectorIndex = Stub;
type EmbeddingService = Stub;

fn auth_service(&self) -> &Self::AuthService {
self
}

fn environment_service(&self) -> &Self::EnvironmentService {
self
}
Expand Down
Loading
Loading