eversal-esi 0.1.2

Eve Online's ESI API library for Rust and Eversal projects
Documentation
use jsonwebtoken::{DecodingKey, TokenData, Validation};
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;
use oauth2::{
  AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields,
  RedirectUrl, Scope, StandardTokenResponse, TokenUrl,
};

use crate::error::Error;
use crate::{client, EsiResult};

const AUTHORIZE_URL: &str = "https://login.eveonline.com/v2/oauth/authorize";
const TOKEN_URL: &str = "https://login.eveonline.com/v2/oauth/token";
const AUTHORIZATION_SERVER_URL: &str =
  "https://login.eveonline.com/.well-known/oauth-authorization-server";
const LOGIN_URL: &str = "https://login.eveonline.com";

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct AuthenticationData {
  pub login_url: String,
  pub state: String,
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct EveSsoMetaData {
  pub authorization_endpoint: String,
  pub code_challenge_methods_supported: Vec<String>,
  pub issuer: String,
  pub jwks_uri: String,
  pub response_types_supported: Vec<String>,
  pub revocation_endpoint: String,
  pub revocation_endpoint_auth_methods_supported: Vec<String>,
  pub token_endpoint: String,
  pub token_endpoint_auth_methods_supported: Vec<String>,
  pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct EveJwtKeys {
  #[serde(rename = "SkipUnresolvedJsonWebKeys")]
  pub skip_unresolved_json_web_keys: bool,
  pub keys: Vec<EveJwtKey>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
#[serde(tag = "alg")]
pub enum EveJwtKey {
  RS256 {
    e: String,
    kid: String,
    kty: String,
    n: String,
    r#use: String,
  },
  ES256 {
    crv: String,
    kid: String,
    kty: String,
    r#use: String,
    x: String,
    y: String,
  },
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct EveJwtClaims {
  pub scp: Option<Vec<String>>,
  pub jti: String,
  pub kid: String,
  pub sub: String,
  pub azp: String,
  pub tenant: String,
  pub tier: String,
  pub region: String,
  pub aud: Vec<String>,
  pub name: String,
  pub owner: String,
  pub exp: u64,
  pub iat: u64,
  pub iss: String,
}

pub fn create_login_url(
  client_id: String,
  client_secret: String,
  callback_url: String,
  scopes: Vec<String>,
) -> EsiResult<AuthenticationData> {
  fn convert_scopes(scopes: Vec<String>) -> Vec<Scope> {
    scopes.iter().map(|s| Scope::new(s.clone())).collect()
  }

  let client = BasicClient::new(
    ClientId::new(client_id),
    Some(ClientSecret::new(client_secret)),
    AuthUrl::new(AUTHORIZE_URL.to_string())?,
    Some(TokenUrl::new(TOKEN_URL.to_string())?),
  )
  .set_redirect_uri(RedirectUrl::new(callback_url)?);

  let scopes = convert_scopes(scopes);

  let (eve_oauth_url, csrf_token) = client
    .authorize_url(CsrfToken::new_random)
    .add_scopes(scopes)
    .url();

  Ok(AuthenticationData {
    login_url: eve_oauth_url.to_string(),
    state: csrf_token.secret().to_string(),
  })
}

pub async fn get_token(
  client_id: String,
  client_secret: String,
  code: String,
) -> EsiResult<StandardTokenResponse<EmptyExtraTokenFields, oauth2::basic::BasicTokenType>> {
  let client = BasicClient::new(
    ClientId::new(client_id),
    Some(ClientSecret::new(client_secret)),
    AuthUrl::new(AUTHORIZE_URL.to_string())?,
    Some(TokenUrl::new(TOKEN_URL.to_string())?),
  );

  match client
    .exchange_code(AuthorizationCode::new(code.to_string()))
    .request_async(async_http_client)
    .await
  {
    Ok(token) => Ok(token),
    Err(err) => return Err(Error::new(err.to_string())),
  }
}

// pub async fn refresh_token(
//   client_id: String,
//   refresh_token: String,
// ) -> EsiResult<StandardTokenResponse<EmptyExtraTokenFields, oauth2::basic::BasicTokenType>> {
//   let client = BasicClient::new(
//     ClientId::new(client_id),
//     None,
//     AuthUrl::new(AUTHORIZE_URL.to_string())?,
//     Some(TokenUrl::new(TOKEN_URL.to_string())?),
//   );

//   match client
//     .exchange_refresh_token(&RefreshToken::new(refresh_token))
//     .request_async(async_http_client)
//     .await
//   {
//     Ok(token) => Ok(token),
//     Err(err) => return Err(EsiError::new(err.to_string())),
//   }
// }

#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct RefreshTokenResponse {
  pub access_token: String,
  pub token_type: String,
  pub expires_in: i64,
  pub refresh_token: String,
}

pub async fn token_refresh(token_id: &str) -> EsiResult<RefreshTokenResponse> {
  println!("Token ID: {}", token_id);
  // This doesn't work, just return an error for now
  Err(Error::new("Failed to refresh token".to_string()))
  // let client_id = env::var("ESI_CLIENT_ID").expect("ESI_CLIENT_ID must be set");
  // let client_secret = env::var("ESI_CLIENT_SECRET").expect("ESI_CLIENT_SECRET must be set");
  // let url = format!("{}?grant_type=refresh_token&refresh_token={}", TOKEN_URL, refresh_token);

  // let b64 = general_purpose::STANDARD.encode(format!("{}:{}", client_id, client_secret).as_bytes());
  // println!("URL: {}", url);

  // let res: RefreshTokenResponse = client().post(url)
  //   .header("Content-Type", "application/x-www-form-urlencoded")
  //   .header("Host", "login.eveonline.com")
  //   .header("Authorization", format!("Basic {}", b64))
  //   .send().await?.json().await?;

  // Ok(res)
}

pub async fn validate_token(token: &str) -> EsiResult<TokenData<EveJwtClaims>> {
  async fn get_eve_jwt_keys() -> EsiResult<EveJwtKeys> {
    let sso_meta_data_url = AUTHORIZATION_SERVER_URL;

    let res: EveSsoMetaData = client().get(sso_meta_data_url).send().await?.json().await?;

    let response = client().get(res.jwks_uri).send().await?.json().await?;

    Ok(response)
  }

  let jwk_keys = get_eve_jwt_keys().await?;
  let jwk_key = match select_key(jwk_keys.keys) {
    Some(key) => key,
    None => return Err(Error::new("Failed to find RS256 key".to_string())),
  };

  let jwk_n: String;
  let jwk_e: String;

  if let EveJwtKey::RS256 {
    e,
    kid: _,
    kty: _,
    n,
    r#use: _,
  } = jwk_key
  {
    jwk_n = n;
    jwk_e = e;
  } else {
    return Err(Error::new("Failed to find RS256 key".to_string()));
  }

  let mut validation = Validation::new(jsonwebtoken::Algorithm::RS256);
  validation.set_audience(&["EVE Online"]);
  validation.set_issuer(&[LOGIN_URL]);

  let decoding_key = &DecodingKey::from_rsa_components(&jwk_n, &jwk_e)?;

  let token = jsonwebtoken::decode::<EveJwtClaims>(&token, decoding_key, &validation)?;

  Ok(token)
}

fn select_key(keys: Vec<EveJwtKey>) -> Option<EveJwtKey> {
  for key in keys {
    if let EveJwtKey::RS256 {
      e: _,
      kid: _,
      kty: _,
      n: _,
      r#use: _,
    } = &key
    {
      return Some(key);
    }
  }

  None
}