eve_esi 0.4.9

Thread-safe, asynchronous client for EVE Online's ESI & OAuth2
Documentation
//! # EVE Online OAuth2 Login
//!
//! Provides the method to create a login URL to begin the EVE Online single sign-on (SSO) process.
//! See the [`OAuth2Endpoints::login_url`] method for details.
//!
//! For an overview & usage examples of OAuth2 with the `eve_esi` crate, see the [module-level documentation](super)
//!
//! ## Usage Example
//!
//! This demonstrates an example of a login API route implemented in the Axum web framework.
//! See [SSO example](https://github.com/hyziri/eve_esi/blob/main/examples/sso.rs) for a more complete demonstration.
//!
//! ```no_run
//! use axum::extract::Extension;
//! use axum::http::StatusCode;
//! use axum::response::{IntoResponse, Response, Redirect};
//! use axum::Json;
//!
//! // A login API route implemented in the Axum web framework
//! async fn login(Extension(esi_client): Extension<eve_esi::Client>) -> Response {
//!    // Build the scopes we wish to request from the user
//!    let scopes = eve_esi::ScopeBuilder::new().public_data().build();
//!
//!    // Generate the login url or return an error if one occurs
//!    let login_url = match esi_client.oauth2().login_url(scopes) {
//!        Ok(login_url) => login_url,
//!        // If OAuth2 is not properly configured for the ESI client then an error will be returned
//!        Err(err) => {
//!            println!("Error initiating OAuth login: {}", err);
//!
//!            return (
//!                StatusCode::INTERNAL_SERVER_ERROR,
//!                Json(serde_json::json!({ "error": "Internal Server Error" })),
//!            )
//!                .into_response();
//!        }
//!    };
//!
//!    // Redirect the user to the login url to begin the single sign-on (OAuth2) login with EVE Online
//!    Redirect::temporary(&login_url.login_url).into_response()
//! }

use oauth2::{CsrfToken, Scope};

use crate::error::{Error, OAuthError};
use crate::model::oauth2::AuthenticationData;
use crate::oauth2::OAuth2Endpoints;

impl<'a> OAuth2Endpoints<'a> {
    /// Generates a login URL and state string for initiating the EVE Online OAuth2 authentication process.
    ///
    /// This method constructs the URL that begins the login process for EVE Online SSO (single sign-on) or also known as OAuth2.
    /// After successful authentication, EVE Online will redirect the user to the callback URL (`callback_url`) specified
    /// in your [`Client`](crate::Client) configuration with an authorization code used to request an access token with the
    /// [crate::oauth2::OAuth2Endpoints::get_token] method.
    ///
    /// # Arguments
    /// - `scopes` (`Vec<`[`String`]`>`): A vec of scope strings representing the permissions your application is requesting.
    ///   These must match the scopes configured in your EVE developer application.
    ///
    /// # Returns
    /// Returns a [`AuthenticationData`] struct containing:
    /// - `login_url` ([`String`]): The URL users should visit to authenticate.
    /// - `state` ([`String`]): A unique state string used for CSRF protection.
    pub fn login_url(&self, scopes: Vec<String>) -> Result<AuthenticationData, Error> {
        // Retrieve the OAuth2 client from the Client
        let client = match &self.client.inner.oauth2_client {
            Some(client) => client,
            // Returns an error if the OAuth2 client is not found due to it not having been configured when
            // building the Client.
            None => {
                error!(
                    "Error building a login URL: {:#?}",
                    OAuthError::OAuth2NotConfigured
                );

                return Err(Error::OAuthError(OAuthError::OAuth2NotConfigured));
            }
        };

        // Convert the Vec<String> of scopes into Vec<Scope>
        let scopes: Vec<Scope> = scopes.into_iter().map(Scope::new).collect();

        // Create the login url & a CSRF state code
        let (eve_oauth_url, csrf_token) = client
            .authorize_url(CsrfToken::new_random)
            .add_scopes(scopes)
            .url();

        // Return login url & state code
        Ok(AuthenticationData {
            login_url: eve_oauth_url.to_string(),
            state: csrf_token.secret().to_string(),
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::error::{Error, OAuthError};
    use crate::ScopeBuilder;

    /// Tests the successful generation of an OAuth2 login URL and CSRF state token.
    ///
    /// # Test Setup
    /// - Configure [`Client`](crate::Client) for OAuth2 with a client_id, client_secret, and callback_url
    /// - Build scopes requesting only publicData
    ///
    /// # Assertions
    /// - Verifies that the generated state token has a non-zero length,
    ///   confirming that proper CSRF protection is in place
    #[test]
    fn test_successful_login_url() {
        // Configure Client for OAuth2 with a client_id, client_secret, and callback_url
        let esi_client = crate::Client::builder()
            .user_agent("MyApp/1.0 (contact@example.com)")
            .client_id("client_id")
            .client_secret("client_secret")
            .callback_url("http://localhost:8080/callback")
            .build()
            .expect("Failed to build Client");

        // Build scopes requesting only publicData
        let scopes = ScopeBuilder::new().public_data().build();

        // Get a login URL
        let result = esi_client.oauth2().login_url(scopes);

        // Assert result is ok
        assert!(result.is_ok());
    }

    /// Ensures the proper error is received when attempting to generate a login url without configuring OAuth2
    ///
    /// # Test Setup
    /// - Create an [`Client`](crate::Client) without setting the client_id, client_secret, or callback_url
    /// - Build scopes requesting only publicData
    ///
    /// # Assertions
    /// - Assert result is an error
    /// - Ensure error is of type EsiError::OAuthError(OAuthError::OAuth2NotConfigured)
    #[test]
    fn test_oauth_client_not_configured() {
        // Create an ESI client without setting the client_id, client_secret, or callback_url
        let esi_client = crate::Client::builder()
            .user_agent("MyApp/1.0 (contact@example.com)")
            .build()
            .expect("Failed to build Client");

        // Build scopes requesting only publicData
        let scopes = ScopeBuilder::new().public_data().build();

        // Get a login URL
        let result = esi_client.oauth2().login_url(scopes);

        // Assert result is an error
        assert!(result.is_err());

        // Ensure error is of type EsiError::OAuthError(OAuthError::OAuth2NotConfigured)
        assert!(matches!(
            result,
            Err(Error::OAuthError(OAuthError::OAuth2NotConfigured))
        ));
    }
}