rustdesk / rustdesk-server-pro

Some scripts for RustDesk Server Pro are hosted here.
107 stars 48 forks source link

Azure OIDC stopped working #269

Open haarhoff-frs opened 2 weeks ago

haarhoff-frs commented 2 weeks ago

Hello,

We use RustDesk Pro in Docker on a Ubuntu 20.04 machine. I upgraded to the latest version by pulling the "latest" tag again. Our users log-in via Azure/Entra OIDC, which worked without a problem until today. We have not made any changes to the app configuration in Azure, and the secret is still valid. When trying to log in in the RustDesk app or our self-hosted web interface, the following error is produced by RustDesk:

Failed to verify ID token, Signature verification failed

Restarting the containers did not help. Any idea how to get to the bottom of this?

fufesou commented 2 weeks ago

@haarhoff-frs Hi, sorry for late.

Can you please have look at the logs? It details the reasons for the failure.

grep  'Failed to verify ID token' *
fufesou commented 2 weeks ago

Can you please also test this simple demo?

The demo

simple_oidc.tar.gz

1718414407011
  1. Download the attached file.
  2. Change the redirect(callback) url in azure to http://localhost:18032
  3. tar -zxf simple_oidc.tar.gz and then run it.
  4. Open the url in your browser
1718414647632
  1. The address in browser will redirect to http://localhost:18032/?code={code}&state={state} . If it cannot redirect. You can copy the url, and run directly in the command line curl http://localhost:18032/?code={code}&state={state} .

The code

You can also build yourself if you can run rust code.

use log::LevelFilter;
use openidconnect::{
    core::{
        CoreClient, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata, CoreResponseType,
    },
    reqwest::http_client,
    AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
    OAuth2TokenResponse, RedirectUrl, Scope,
};
use std::{
    fmt::Debug, io::{BufRead, BufReader, Write}, net::TcpListener, process::exit
};
use structopt::{clap::AppSettings, StructOpt};
use url::Url;

fn handle_error<T: std::error::Error + Debug>(fail: &T, msg: &'static str) {
    let mut err_msg = format!("ERROR: {}", msg);
    let mut cur_fail: Option<&dyn std::error::Error> = Some(fail);
    while let Some(cause) = cur_fail {
        err_msg += &format!("\n    caused by: {}", cause);
        cur_fail = cause.source();
    }
    println!("{}", err_msg);
    println!("More detailed error: {:?}", fail);
    exit(1);
}

#[derive(StructOpt, Debug)]
#[structopt(name = "lettre-test",
            about = "Test oidc connect.",
            rename_all = "kebab-case",
            setting = AppSettings::ColoredHelp)]
struct Options {
    /// The log level. t - trace, d - debug, i - info, w - warn, e - error. Default is "d".
    #[structopt(long, short)]
    loglevel: Option<String>,

    /// Client ID.
    #[structopt(long, short = "c")]
    client_id: String,

    /// Client secret.
    ///
    #[structopt(long, short = "C")]
    client_secret: String,

    /// The privider's issuer URL.
    ///
    #[structopt(long, short)]
    issuer_url: String,
}

fn main() {
    let options = Options::from_args();

    let redirect_port = 18032;
    let redirect_url = format!("http://localhost:{}", redirect_port);

    println!("================== Please hide secrets!!! =======================");
    println!(
        "The redirect URL: {redirect_url}, please change it in the OIDC provider's settings page."
    );
    println!("The options: {:?}", &options);

    let level = match options.loglevel.as_ref().unwrap_or(&String::from("d")) as &str {
        "t" => LevelFilter::Trace,
        "d" => LevelFilter::Debug,
        "i" => LevelFilter::Info,
        "w" => LevelFilter::Warn,
        "e" => LevelFilter::Error,
        _ => LevelFilter::Debug,
    };
    env_logger::builder().filter_level(level).init();

    let client_id = ClientId::new(options.client_id);
    let client_secret = ClientSecret::new(options.client_secret);
    let issuer_url = IssuerUrl::new(options.issuer_url).unwrap_or_else(|err| {
        handle_error(&err, "Invalid issuer URL");
        unreachable!();
    });

    let provider_metadata = CoreProviderMetadata::discover(&issuer_url, http_client)
        .unwrap_or_else(|err| {
            handle_error(&err, "Failed to discover OpenID Provider");
            unreachable!();
        });

    // Set up the config for the Google OAuth2 process.
    let client =
        CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret))
            // This example will be running its own server at localhost:18032.
            // See below for the server implementation.
            .set_redirect_uri(
                RedirectUrl::new(redirect_url.to_string()).expect("Invalid redirect URL"),
            );

    // Generate the authorization URL to which we'll redirect the user.
    let (authorize_url, csrf_state, nonce) = client
        .authorize_url(
            AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
            CsrfToken::new_random,
            Nonce::new_random,
        )
        // This example is requesting access to the "calendar" features and the user's profile.
        .add_scope(Scope::new("email".to_string()))
        .add_scope(Scope::new("profile".to_string()))
        .url();

    println!("Open this URL in your browser:\n{}\n", authorize_url);

    // A very naive implementation of the redirect server.
    let listener = TcpListener::bind(format!("127.0.0.1:{}", redirect_port)).unwrap();

    // Accept one connection
    let (mut stream, _) = listener.accept().unwrap();

    let code;
    let state;
    {
        let mut reader = BufReader::new(&stream);

        let mut request_line = String::new();
        reader.read_line(&mut request_line).unwrap();

        let redirect_url = request_line.split_whitespace().nth(1).unwrap();
        let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();

        let code_pair = url
            .query_pairs()
            .find(|pair| {
                let &(ref key, _) = pair;
                key == "code"
            })
            .unwrap();

        let (_, value) = code_pair;
        code = AuthorizationCode::new(value.into_owned());

        let state_pair = url
            .query_pairs()
            .find(|pair| {
                let &(ref key, _) = pair;
                key == "state"
            })
            .unwrap();

        let (_, value) = state_pair;
        state = CsrfToken::new(value.into_owned());
    }

    let message = "Go back to your terminal :)";
    let response = format!(
        "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
        message.len(),
        message
    );
    stream.write_all(response.as_bytes()).unwrap();

    println!(" The OP returned the following code:\n{}\n", code.secret());
    println!(
        " The OP returned the following state:\n{} (expected `{}`)\n",
        state.secret(),
        csrf_state.secret()
    );

    // Exchange the code with a token.
    let token_response = client
        .exchange_code(code)
        .request(http_client)
        .unwrap_or_else(|err| {
            handle_error(&err, "Failed to contact token endpoint");
            unreachable!();
        });

    println!(
        " The OP returned access token:\n{}\n",
        token_response.access_token().secret()
    );
    println!(" The OP returned scopes: {:?}", token_response.scopes());

    let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier();
    let id_token_claims: &CoreIdTokenClaims = token_response
        .extra_fields()
        .id_token()
        .expect("Server did not return an ID token")
        .claims(&id_token_verifier, &nonce)
        .unwrap_or_else(|err| {
            handle_error(&err, "Failed to verify ID token");
            unreachable!();
        });
    println!(" The OP returned ID token: {:?}", id_token_claims);

    println!("================== Please hide secrets!!! =======================");
    println!("================== Please hide secrets!!! =======================");
    println!("================== Please hide secrets!!! =======================");
}

Cargo.toml

[package]
name = "simple_oidc"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
regex = "1.8"
ctrlc = "3.1.9"
signal-hook = "0.3.10"
structopt = { version = "0.3.26", features = ["wrap_help"] }
lettre =  { version = "0.10", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "pool", "hostname", "builder"] }
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots"], default-features=false }
tokio = { version = "1.33", features = ["full"] }
openidconnect = { git = "https://github.com/fufesou/openidconnect-rs", branch="refact/ignore_timestamp_error_for_auth0", features = [
    "reqwest",
    "rustls-tls"
], default-features=false }
serde = "1.0.189"
url = "2.4.1"
env_logger = "0.10.0"
selinux = "0.4.2"
log = "0.4.20"
haarhoff-frs commented 2 weeks ago

It seems to have been a transient error of some kind. I tried to create a new log entry, as the logs are only stored inside the container, and was able to login this morning. I'll try to recreate the error by restarting the server tonight and will report back.