Skip to content

Add Interactive Browser Authentication Support #2418

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions sdk/identity/azure_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ url.workspace = true
[target.'cfg(unix)'.dependencies]
tz-rs = { workspace = true, optional = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-process.workspace = true

[dev-dependencies]
azure_core_test.workspace = true
azure_security_keyvault_secrets = { path = "../../keyvault/azure_security_keyvault_secrets" }
Expand Down
40 changes: 40 additions & 0 deletions sdk/identity/azure_identity/examples/interactive_credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use azure_identity::interactive_credential::interactive_browser_credential::InteractiveBrowserCredential;
use oauth2::TokenResponse;
use reqwest::Client;
use std::error::Error;
use url::Url;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let test_subscription_id =
std::env::var("AZURE_SUBSCRIPTION_ID").expect("AZURE_SUBSCRIPTION_ID required");
let test_tenant_id = std::env::var("AZURE_TENANT_ID").expect("AZURE_TENANT_ID required");

let _ = run_app_inter(test_subscription_id, test_tenant_id).await?;
Ok(())
}

async fn run_app_inter(subscription_id: String, tenant_id: String) -> Result<(), Box<dyn Error>> {
let interactive_credentials = InteractiveBrowserCredential::new(None, Some(tenant_id), None)?;

let token_response = interactive_credentials
.get_token(Some(&["https://management.azure.com/.default"]))
.await?;

let access_token_secret = token_response.access_token().secret();

let url = Url::parse(&format!(
"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Storage/storageAccounts?api-version=2019-06-01"
))?;

let resp = Client::new()
.get(url)
.header("Authorization", format!("Bearer {}", access_token_secret))
.send()
.await?
.text()
.await?;

println!("Res interactive: {resp}");
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use super::internal_server::*;
use crate::authorization_code_flow;
use azure_core::{error::ErrorKind, http::new_http_client, http::Url, Error};
use oauth2::{
basic::BasicTokenType, AuthorizationCode, ClientId, EmptyExtraTokenFields,
StandardTokenResponse,
};
use std::{str::FromStr, sync::Arc};

/// Default OAuth scopes used when none are provided.
#[allow(dead_code)]
const DEFAULT_SCOPE_ARR: [&str; 3] = ["openid", "offline_access", "profile"];
/// Default client ID for interactive browser authentication.
#[allow(dead_code)]
const DEFAULT_DEVELOPER_SIGNON_CLIENT_ID: &str = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
/// Default tenant ID used when none is specified.
#[allow(dead_code)]
const DEFAULT_ORGANIZATIONS_TENANT_ID: &str = "organizations";

/// Provides interactive browser-based authentication.
#[derive(Clone)]
pub struct InteractiveBrowserCredential {
/// Client ID of the application.
pub(crate) client_id: ClientId,
/// Tenant ID for the authentication request.
pub(crate) tenant_id: String,
/// Redirect URI where the authentication response is sent.
pub(crate) redirect_url: Url,
}

impl InteractiveBrowserCredential {
/// Creates a new `InteractiveBrowserCredential` instance with optional parameters.
pub fn new(
client_id: Option<ClientId>,
tenant_id: Option<String>,
redirect_url: Option<Url>,
Comment on lines +34 to +36
Copy link
Member

Choose a reason for hiding this comment

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

These should be passed into an InteractiveBrowserCredentialOptions per guidelines and some of the newer credential types/changes.

) -> azure_core::Result<Arc<Self>> {
let client_id = client_id
.unwrap_or_else(|| ClientId::new(DEFAULT_DEVELOPER_SIGNON_CLIENT_ID.to_owned()));

let tenant_id = tenant_id.unwrap_or_else(|| DEFAULT_ORGANIZATIONS_TENANT_ID.to_owned());

let redirect_url = redirect_url.unwrap_or_else(|| {
Url::from_str(&format!("http://localhost:{}", LOCAL_SERVER_PORT))
.expect("Failed to parse redirect URL")
});

Ok(Arc::new(Self {
client_id,
tenant_id,
redirect_url,
}))
}

/// Starts the interactive browser authentication flow and returns an access token.
///
/// If no scopes are provided, default scopes will be used.
#[allow(dead_code)]
pub async fn get_token(
Copy link
Member

Choose a reason for hiding this comment

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

This should be wrapped in the internal cache to prevent every call redundantly authenticating. See for example ClientAssertionCredential.

Here's a gotcha though: the cache implicitly assumes it holds tokens for only one identity. That won't work for this particular credential because users may choose a different account every time they authenticate. We have to prevent this credential getting into a state where, say, it returns Cosmos tokens for user A and Key Vault tokens for user B. I imagine the simplest solution would be having the credential remember the last authenticated user (this would require parsing id tokens) and clear the cache when the application authenticates a new user. That may be complex to implement

Copy link
Author

Choose a reason for hiding this comment

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

Hi @chlowell , I've now implemented the cache similar to how it's done in ` ClientAssertionCredential. However, I'm still trying to fully understand the concern regarding caching tokens from potentially different identities.

From my current perspective, it seems that when someone wants to authenticate with a different account (e.g., switching from user A to user B), they would typically create a new instance of InteractiveBrowserCredential, configured with the appropriate options. Based on this assumption, I thought the risk of mixing tokens between different users would be naturally avoided. Here's a simplified example of how I imagined this working:

fn foo(){
// User A
let options_a = InteractiveBrowserCredentialOptions {
    client_id: None,
    tenant_id: Some(tenant_id_a),
    redirect_url: None,
};
let credential_a = InteractiveBrowserCredential::new(options_a)?;
let token_a = credential_a.get_token(&["https://management.azure.com/.default"]).await?;

// Do something with token_a...

// User B
let options_b = InteractiveBrowserCredentialOptions {
    client_id: None,
    tenant_id: Some(tenant_id_b),
    redirect_url: None,
};
let credential_b = InteractiveBrowserCredential::new(options_b)?;
let token_b = credential_b.get_token(&["https://management.azure.com/.default"]).await?;

// Do something with token_b...
}

So my (possibly naive) assumption was: if the developer needs to authenticate a different user, they would instantiate a separate credential, similar to how other credentials behave.

That said, I might very well be missing an important part of the picture. I'm still relatively new to this, and I want to make sure I understand the full context. If you have time, could you perhaps elaborate a bit more on the potential issue – maybe even with some pseudocode or a concrete example where this problem could arise?

Thanks again for your guidance and patience. I really appreciate it and I’m learning a lot through this process.

Copy link
Member

Choose a reason for hiding this comment

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

So my (possibly naive) assumption was: if the developer needs to authenticate a different user, they would instantiate a separate credential, similar to how other credentials behave.

That assumption doesn't hold for an interactive credential like this because the developer can't control the authenticated account or know it before get_token returns. This credential sends the user to a web page where they can log in any account they want. In your example, credential_a and credential_b could authenticate the same account or different accounts--that's up to the user when they complete the interactive login.

You're correct that a developer who wanted to get tokens for different users simultaneously would need multiple instances of this credential, however that isn't a common scenario and would be difficult to accomplish because again, the user (of the program) completes the interactive login and therefore decides which account to authenticate. The common case is a single-user program like the Azure CLI, a tool users run locally to do something with Azure resources.

I should reiterate that implementing this credential is particularly difficult because it has more moving parts than other types and entails additional work for caching and handling user accounts, parts of which may not be documented. Honestly, it's a bad first issue.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @chlowell, thank you so much for the detailed explanation. I really appreciate it!

I realize now that I had a bit of a misunderstanding around the caching behavior. I assumed that caching would inherently avoid the situation where different users get mixed up, because once a token is cached, no new browser flow would be triggered – so no opportunity for a different user to log in.
What I didn’t quite see is that once the token expires, a new login can happen, and that’s where a different user could authenticate. And because the cache isn’t user-aware, that can lead to the wrong token being used or stored. That totally makes sense now.

So the correct behavior would be: the credential needs to detect when a different user logs in (e.g., by parsing the ID token) and clear the cache when that happens, so tokens from different identities don’t get mixed up. Got it!

Even though this might not be the ideal "first issue" 😅, I’d still really like to give it a proper shot and learn from it. If you agree, I’ll try to model the solution after the existing caching mechanisms like in ClientAssertionCredential and expand from there.

Thanks again for your help and patience! 🙌

Copy link
Member

Choose a reason for hiding this comment

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

So the correct behavior would be: the credential needs to detect when a different user logs in (e.g., by parsing the ID token) and clear the cache when that happens, so tokens from different identities don’t get mixed up.

I believe that's the simplest way to get the correct behavior. An ID token won't always provide the data necessary to identify a user account uniquely and consistently, so you'd instead use the "home account ID", which so far as I know isn't documented. To get one, add client_info=1 to the token request body. The response will then have a field "client_info": "some base64". The base64 decodes to a string of JSON like {"uid":"A","utid":"B"}. A and B are consistent and unique for the authenticated account, and by convention the home account ID is "A.B".

I don't want to discourage you working on this PR--your contribution's welcome--only to make sure you're aware that this feature is particularly complex and getting this PR to the finish line will probably require significant effort all around. Continuing to work on it and abandoning it are both reasonable choices ☺️

Copy link
Author

Choose a reason for hiding this comment

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

@chlowell Thanks a lot for the helpful reply!

That’s really good to know about the ID token. I had just implemented the Hybrid Flow and was planning to cache the oid,tid and also the sub claim. Based on this StackOverflow thread, I know now, that I wasn’t too far off. But if the ID token doesn’t guarantee a stable identifier, then I’ll definitely switch gears and give client_info a try instead, that sounds like the more reliable way to go.

Whether or not this PR ends up being merged is totally fine with me. I’ve already learned so much and that’s a huge win on its own for me 😄. If at some point I start to clutter your inbox too much, feel free to wave the white flag, no hard feelings 😄. For now, I’d still love to keep exploring and pushing it a bit further if that’s okay!

Copy link
Author

Choose a reason for hiding this comment

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

@chlowell, apologies for the delay. I'm just finalizing the changes.
Before wrapping up, I wanted to ask if it would make sense to provide an option for users to choose between using the hybrid auth flow and the approach that includes client_info=1?
I'm asking because I still have the implementation for the hybrid flow and could include it if it's useful.

Copy link
Member

Choose a reason for hiding this comment

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

No, that's an implementation detail. The get_token contract is "scopes in, access token out". User account data isn't relevant at that level because it doesn't affect the access token. Come to think of it, we should avoid acquiring an ID token if possible, because that's a waste of cycles and bytes given we have no use for the token at this time. That would mean not adding the openid and profile scopes. I don't know whether client_info=1 works without those scopes so, please give it a try.

Copy link
Author

@M-Patrone M-Patrone May 11, 2025

Choose a reason for hiding this comment

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

@chlowell I tested the behavior, and it seems that when both openid and profile scopes are removed, I no longer receive the user info (client_info). I also tried including only one of them at a time, but that didn’t work either. It appears that both openid and profile need to be present in order to receive the user info:
image
Given that, acquiring an ID token seems to be unavoidable if we need access to client_info, unless there's another approach I'm missing. Let me know if you have any suggestions on how we might avoid the extra scopes while still retrieving the necessary user data.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for doing the science. We need client_info in any case, so it's okay to acquire an ID token if that's necessary to get client_info.

&self,
scopes: Option<&[&str]>,
Copy link
Member

Choose a reason for hiding this comment

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

This function should require at least one scope and add ["offline_access", "openid", "profile"] to the caller's scopes. There are a few reasons scopes mustn't be optional:

  1. credentials must implement TokenCredential to function with SDK clients
  2. get_token(None) means something nonsensical like "give me an access token, I don't care what it's for"
  3. default scopes can cause mysterious behavior

Copy link
Author

Choose a reason for hiding this comment

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

@chlowell Thank you very much for your feedback, and apologies for the delayed response.

I'm currently working on implementing TokenCredential. However, I’ve run into an issue that I find a bit puzzling. When compiling, I get the following error:

 implementation of `std::marker::Send` is not general enough
   --> sdk/identity/azure_identity/src/interactive_credential/interactive_browser_credential.rs:150:83
    |
150 |       async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
    |  ___________________________________________________________________________________^
151 | |         let scopes_owned: Vec<Cow<'_, str>> = scopes.iter().map(|s| Cow::Borrowed(*s)).collect();
152 | |         self.get_access_token(scopes_owned).await
153 | |     }
    | |_____^ implementation of `std::marker::Send` is not general enough
    |
    = note: `std::marker::Send` would have to be implemented for the type `&str`
    = note: ...but `std::marker::Send` is actually implemented for the type `&'0 str`, for some specific lifetime `'0`

I haven’t had the chance to fully dive into this yet, but maybe someone has encountered a similar issue before. The error seems to be triggered by this part of the code:

authorization_code_flow
                    .exchange(
                        options.http_client.clone(),
                        AuthorizationCode::new(code).clone(),
                    )
                    .await
                    .map(|r| {
                        return AccessToken::new(
                            r.access_token().secret().clone(),
                            OffsetDateTime::now_utc() + r.expires_in().unwrap().clone(),
                        );
                    });

This eventually calls into the function here: exchange

Copy link
Member

Choose a reason for hiding this comment

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

I ran into the same problem last week. It appears related to rust-lang/rust#64552. I gave up on finding a workaround because replacing oauth2 with my own code was much faster. That's what I would do in this case as well. The protocol is documented here, though that doc doesn't cover the public client case we're interested in--in the exchange request, a public client would omit client_secret and client_assertion.

Copy link
Author

Choose a reason for hiding this comment

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

@chlowell Thanks a lot for your helpful response! I really appreciate you taking the time.

Over the last couple of days, I dug into the issue to better understand the root cause—mainly because the original error message was so vague that it didn’t point me in any useful direction. That was the most frustrating part.

So I started isolating the problem step by step. It became clear that the error happens during the exchange call in the authorization code flow. To narrow it down further, I stripped my code down to this minimal example:

let b = AuthorizationCode::new("test_auth_code".to_string()).clone();
 let c = self.options.http_client.clone();
 let a = authorization_code_flow.exchange(c, b).await; 

That’s where the error triggered. I then created a dedicated test function to extract and observe the behavior more closely:

    fn get_access_token_test(
        &self,
        scopes: &[&str],
    ) -> impl Send + Future<Output = azure_core::Result<AccessToken>> {
        assert_send(async move {
            let authorization_code_flow = authorization_code_flow::authorize(
                ClientId::new("jkadjfa".to_string()),
                None,
                &"jkadjfa".to_string(),
                Url::from_str("str").unwrap(),
                &scopes,
            );

            let b = AuthorizationCode::new("djfak".to_string()).clone();
            let c = new_http_client();

            let a = authorization_code_flow.exchange(c, b).await?.clone();

            Ok(AccessToken::new("test", OffsetDateTime::now_utc()))
        })
    }
}

And here's the helper function for asserting Send:

fn assert_send<T>(fut: impl Send + Future<Output = T>) -> impl Send + Future<Output = T> {
    fut
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl TokenCredential for InteractiveBrowserCredential {
    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
        self.get_access_token_test(scopes).await
    }
}

I used this in a custom TokenCredential implementation so I could observe the issue directly when calling get_token.

This gave me a much more helpful error message:

error: implementation of std::marker::Send is not general enough
   --> sdk/identity/azure_identity/src/interactive_credential/interactive_browser_credential.rs:154:9
    |
154 | /         assert_send(async move {
155 | |             let authorization_code_flow = authorization_code_flow::authorize(
156 | |                 ClientId::new("jkadjfa".to_string()),
157 | |                 None,
...   |
168 | |             Ok(AccessToken::new("test", OffsetDateTime::now_utc()))
169 | |         })
    | |__________^ implementation of std::marker::Send is not general enough
    |
    = note: std::marker::Send would have to be implemented for the type &Oauth2HttpClient
    = note: ...but std::marker::Send is actually implemented for the type &'0 Oauth2HttpClient, for some specific lifetime '0

So the issue was clearly a Send problem related to the lifetime of the closure passed to request_async.

To fix it, I restructured my exchange method to use Arc and clone it inside the closure, like this:

 pub async fn exchange(
        self,
        http_client: Arc<dyn HttpClient>,
        code: oauth2::AuthorizationCode,
    ) -> azure_core::Result<
        oauth2::StandardTokenResponse<oauth2::EmptyExtraTokenFields, oauth2::basic::BasicTokenType>,
    > {
        //        let oauth_http_client = Oauth2HttpClient::new(http_client.clone());
        //        let client = |request: HttpRequest| oauth_http_client.request(request);

        //improve problem with implementing the `send`
        let oauth_http_client = Arc::new(Oauth2HttpClient::new(http_client.clone()));
        let client = {
            let oauth_http_client = oauth_http_client.clone();
            move |request: HttpRequest| {
                let oauth_http_client = oauth_http_client.clone();
                async move { oauth_http_client.request(request).await }
            }
        };

        self.client
            .exchange_code(code)
            // Send the PKCE code verifier in the token request
            .set_pkce_verifier(self.pkce_code_verifier)
            .request_async(&client)
            .await
            .context(
                ErrorKind::Credential,
                "exchanging an authorization code for a token failed",
            )
    }

This resolved the Send issue for now—though I still want to run a few more tests to be completely sure.

Thanks again for your time and suggestion to replace the oauth2 crate. That’s still on the table, but I wanted to understand the root problem first—and I think I’ve got a clearer picture now.

Copy link
Member

Choose a reason for hiding this comment

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

I'm sorry to make this feature bigger, however it will also need an auth code flow implementation because we want to remove the oauth2 dependency. The flow is documented here

Copy link
Author

Choose a reason for hiding this comment

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

@chlowell, no worries at all. I really appreciate your guidance throughout this process.

Digging into this implementation has been quite insightful for me. It also gave me the opportunity to learn how to handle custom OAuth2 properties returned in the token response body, such as client_info 😁. To achieve this, I extended the OAuth2 client using a custom oauth2_http_client::Oauth2HttpClient and defined a new client type to work with the additional fields.

Based on your comment, I understand that you want to remove the oauth2 dependency entirely and implement the authorization code flow ourselves.

That brings me to a quick question: Would you prefer that I

  • implement a general authorization code flow in a separate PR and then extend it for the interactive flow (to handle client_info),
  • or focus only on implementing the interactive variant directly with client_info,
  • or possibly handle everything directly in this current PR?

I'm totally flexible and happy to go with whichever option fits the project's needs and your preferences best — just let me know how you'd like to proceed.

Copy link
Member

Choose a reason for hiding this comment

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

I would add it in this PR. In my imagination it isn't a lot of code because you can assume the server is Entra ID (e.g. simply request client_info in all cases) and that only this credential will use the auth code flow.

) -> azure_core::Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>> {
let scopes = scopes.unwrap_or(&DEFAULT_SCOPE_ARR);

let authorization_code_flow = authorization_code_flow::authorize(
self.client_id.clone(),
None,
&self.tenant_id,
self.redirect_url.clone(),
scopes,
);

let auth_code = open_url(authorization_code_flow.authorize_url.as_ref()).await;

match auth_code {
Some(code) => {
authorization_code_flow
.exchange(new_http_client(), AuthorizationCode::new(code))
.await
}
None => Err(Error::message(
ErrorKind::Other,
"Failed to retrieve authorization code.",
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::debug;
use tracing::Level;
use tracing_subscriber;
static INIT: std::sync::Once = std::sync::Once::new();

fn init_tracing() {
INIT.call_once(|| {
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
});
}

#[tokio::test]
async fn interactive_auth_flow_should_return_token() {
Copy link
Member

Choose a reason for hiding this comment

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

This mustn't run by default. CI runs can't complete the interactive login and we don't want to open a browser every time someone working on the crate runs cargo test

Copy link
Member

Choose a reason for hiding this comment

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

This would be a good place for something under examples/ with a comment here in interactive_browser_credential.rs asking devs changing the code to run it manually. You should put assertions in there - at least that it works.

init_tracing();
debug!("Starting interactive authentication test");

let credential = InteractiveBrowserCredential::new(None, None, None)
.expect("Failed to create credential");

let token_response = credential.get_token(None).await;
debug!("Authentication result: {:#?}", token_response);
assert!(token_response.is_ok());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
use std::io::{self, BufRead, BufReader, Write};
use std::net::{Shutdown, TcpListener, TcpStream};
use std::time::Duration;
use tracing::{error, info};

///The port where the local server is listening on the auth_code
#[allow(dead_code)]
pub const LOCAL_SERVER_PORT: u16 = 47828;

/// Opens the given URL in the default system browser and starts a local web server
/// to receive the authorization code.
#[allow(dead_code)]
#[cfg(target_os = "windows")]
pub async fn open_url(url: &str) -> Option<String> {
use async_process::Command;
let spawned = Command::new("cmd").args(["/C", "explorer", url]).spawn();
handle_browser_command(spawned)
}

/// Opens the given URL in the default system browser and starts a local web server
/// to receive the authorization code.
#[allow(dead_code)]
#[cfg(target_os = "macos")]
pub async fn open_url(url: &str) -> Option<String> {
use async_process::Command;
let spawned = Command::new("open").arg(url).spawn();
handle_browser_command(spawned)
}

/// Opens the given URL in the default system browser and starts a local web server
/// to receive the authorization code.
#[allow(dead_code)]
#[cfg(target_os = "linux")]
pub async fn open_url(url: &str) -> Option<String> {
use async_process::Command;

if let Some(command) = find_linux_browser_command().await {
let spawned = Command::new(command).arg(url).spawn();
return handle_browser_command(spawned);
}

info!("Open the following link manually in your browser: {url}");
None
}

/// Method to check if the command to open the link in a browser is available on the computer
/// exists.
#[allow(dead_code)]
#[cfg(target_os = "linux")]
async fn is_command_available(cmd: &str) -> bool {
use async_process::Command;
Command::new("which")
.arg(cmd)
.output()
.await
.map(|o| !o.stdout.is_empty())
.unwrap_or(false)
}

/// Method with all the commands which could open the browser to call the authorization url
/// If there is no command installed or available on the system, it returns a 'None' and the link
/// will be logged
#[allow(dead_code)]
#[cfg(target_os = "linux")]
async fn find_linux_browser_command() -> Option<String> {
let candidates = [
"xdg-open",
"gnome-open",
"kfmclient",
"microsoft-edge",
"wslview",
];
for cmd in candidates.iter() {
if is_command_available(cmd).await {
return Some(cmd.to_string());
}
}
None
}

/// starting the browser if the browser could be started, then the webserver should be started to
/// get the auth code
#[allow(dead_code)]
fn handle_browser_command(result: Result<async_process::Child, io::Error>) -> Option<String> {
match result {
Ok(_) => start_webserver(),
Err(e) => {
error!("Failed to start browser command: {e}");
None
}
}
}

/// Starts the webserver on the `http://localhost`. Returns None, if the server could not have
/// started
#[allow(dead_code)]
/// Starts a simple HTTP server on localhost to receive the auth code.
fn start_webserver() -> Option<String> {
TcpListener::bind(("127.0.0.1", LOCAL_SERVER_PORT))
.ok()
.and_then(handle_tcp_connection)
}

fn handle_tcp_connection(listener: TcpListener) -> Option<String> {
listener
.incoming()
.take(1)
.next()?
.ok()
.and_then(handle_client)
}
/// Main method to handle the incomming traffic.
/// After a 10s timeout the stream will be closed
/// if the stream could be opened, we read the whole request and try to extract the auth_code
/// Returns also the html code to show if it worked
#[allow(dead_code)]
fn handle_client(mut stream: TcpStream) -> Option<String> {
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.ok()?;

let buf_reader = BufReader::new(&stream);
let mut request_lines = vec![];
for line in buf_reader.lines().map_while(Result::ok) {
if line.is_empty() {
break;
}
request_lines.push(line);
}

let request = request_lines.join("\n");

let auth_code = extract_auth_code(&request);
let response_body = r#"<!DOCTYPE html>
<html><head><title>Auth Complete</title></head>
<body><p>Authentication complete. You may close this tab.</p></body>
</html>"#;

let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n{}",
response_body.len(),
response_body
);

stream.write_all(response.as_bytes()).ok()?;
stream.flush().ok()?;
stream.shutdown(Shutdown::Both).ok()?;

auth_code
}

/// Extracts the `code` query parameter from the request.
#[allow(dead_code)]
fn extract_auth_code(request: &str) -> Option<String> {
let code_start = request.rfind("code=")? + 5;
let rest = &request[code_start..];
let end = rest.find('&').unwrap_or(rest.len());
Some(rest[..end].to_string())
}

#[cfg(test)]
mod test_internal_server {
use super::*;
use tracing::debug;
use tracing::Level;
use tracing_subscriber::FmtSubscriber;
fn init_logger() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
}

#[tokio::test]
async fn test_valid_command() {
init_logger();
assert!(is_command_available("ls").await);
}

#[tokio::test]
async fn test_invalid_command() {
init_logger();
assert!(!is_command_available("non_existing_command_foo").await);
}

#[test]
fn test_extract_code_param() {
let url = "GET /?code=abc123&state=xyz";
assert_eq!(extract_auth_code(url).unwrap(), "abc123");
}

#[test]
fn test_extract_code_at_end() {
let url = "GET /?state=xyz&code=abc123";
assert_eq!(extract_auth_code(url).unwrap(), "abc123");
}

#[test]
fn test_extract_code_missing() {
let url = "GET /?state=only";
assert!(extract_auth_code(url).is_none());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod internal_server;

pub mod interactive_browser_credential;
1 change: 1 addition & 0 deletions sdk/identity/azure_identity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod chained_token_credential;
mod credentials;
mod env;
mod federated_credentials_flow;
pub mod interactive_credential;
mod oauth2_http_client;
mod refresh_token;
mod timeout;
Expand Down
Loading