lpotthast / axum-keycloak-auth

Protect axum routes with a JWT emitted by Keycloak.
https://crates.io/crates/axum-keycloak-auth
Apache License 2.0
34 stars 13 forks source link

Does this crate support self signed certificates #24

Open elliot-ast opened 2 months ago

elliot-ast commented 2 months ago

Hello does this crate support self signed certificates. I've gotten this to crate to work when I didn't have TLS on my keycloak server but adding TLS support with following command I can't get this crate to work :man_shrugging:

docker run -p 8443:8443   
-e KC_HTTPS_CERTIFICATE_FILE=/etc/x509/https/tls.crt 
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/x509/https/tls.key  
-e KC_HTTPS_PORT=8443  
-e KC_HTTP_ENABLED=false  
-e KEYCLOAK_ADMIN=admin   
-e KEYCLOAK_ADMIN_PASSWORD=admin  
 -v /self_signed_certs/localhostCert.pem:/etc/x509/https/tls.crt 
 -v /self_signed_certs/localhostKey.pem:/etc/x509/https/tls.key   
quay.io/keycloak/keycloak:24.0.5  start-dev --https-port=8443

were the certificates are created via

openssl req -x509 -out localhostCert.pem. -keyout localhostKey.pem \
  -newkey rsa:2048 -nodes -sha256 \
  -subj '/CN=localhost' -extensions EXT -config <( \
   printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")

and running the standard code from this crate and Axum TLS example where the realm is swapped realm to myrealm instead where there's a user with the realm role administrator. From my JWT

 "allowed-origins": [
    "https://127.0.0.1:3000"
  ],
  "realm_access": {
    "roles": [
      "administrator",
      "default-roles-myapi",
      "offline_access",
      "uma_authorization"
    ]
  }, 

This is the rust code used

[tokio::main]
async fn main() {
    let ports = Ports {
        http: 7878,
        https: 3000,
    };
    // optional: spawn a second server to redirect http requests to this server
    tokio::spawn(redirect_http_to_https(ports));

    // configure certificate and private key used by https
    let config = RustlsConfig::from_pem_file(
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("self_signed_certs")
            .join("localhostCert.pem"),
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("self_signed_certs")
            .join("localhostKey.pem"),
    )
    .await
    .unwrap();

    let keycloak_auth_instance = KeycloakAuthInstance::new(
        KeycloakConfig::builder()
            .server(Url::parse("https://127.0.0.1:8443/").unwrap())
            .realm(String::from("myapi"))
            .build(),
    );
    let app = public_router().merge(protected_router(keycloak_auth_instance));
    let addr = SocketAddr::from(([127, 0, 0, 1], ports.https));
    tracing::debug!("listening on {}", addr);
    axum_server::bind_rustls(addr, config)
        .serve(app.into_make_service())
        .await
        .unwrap();
} 

I don't know what is causing this

curl -k https://127.0.0.1:3000/protected -H "Authorization: Bearer $(cat JWT.txt)" 

gives me the output

{"error":"There were no decoding keys available."}

Is this an issue with my keycloak setup or is it this crate that's not supporting self signed certs?

Tockra commented 2 months ago

Maybe you should access your server with the URL you set at your CN. So don't call 127.0.0.1. Instead use "localhost" or sign a new certifcate with CN 127.0.0.1 . I had this problem in a lot of librarys using SSL/TLS. Sometimes even the CN was not enough. Then the library ignored it and wanted a set SAN : https://support.dnsimple.com/articles/what-is-ssl-san/

Maybe this could already solute your problem. If not, pleas provide some logging of your application. What does it show while it can not access your keycloaks public key?

elliot-ast commented 2 months ago

Alright thanks for looking into this, I've tried to generate the pem files with openssl.cnf file instead with

openssl req -x509 -out localhostCert.pem -keyout localhostKey.pem \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=localhost' -extensions EXT -config openssl.cnf

where my config looks like

[ req ]
default_bits = 2048
default_md = sha256
prompt = no
distinguished_name = dn
req_extensions = req_ext
x509_extensions = v3_ca

[ dn ]
CN = localhost

[ req_ext ]
subjectAltName = @alt_names

[ v3_ca ]
subjectAltName = @alt_names
keyUsage = digitalSignature, keyCertSign, cRLSign
basicConstraints = CA:TRUE

[ alt_names ]
DNS.1 = localhost
IP.1 = 127.0.0.1

the issue persist even though I've altered everything in the rust files to use localhost instead of the 127.0.0.1 this is probably not an issue with this crate and I guess it would work when using signed certificates. Unless there is some setting I need to enable in keycloak?

Not sure what kind of logging you want from the keycloak/rust app that I can provide you with could you elaborate on this?

Tockra commented 2 months ago

If I include the middleware into my application and setup a logging crate like fern I'll get a lot of debug messages when I run my application: e.g. execute the following function in the top of your main function.

pub fn setup_logger() -> Result<(), fern::InitError> {
    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "[{} {} {}] {}",
                humantime::format_rfc3339_seconds(SystemTime::now()),
                record.level(),
                record.target(),
                message
            ))
        })
        .level(log::LevelFilter::Debug)
        .chain(std::io::stdout())
        .chain(fern::log_file("output.log")?)
        .apply()?;
    Ok(())
}

https://crates.io/crates/fern

[...]
[2024-06-12T09:58:12Z DEBUG hyper_util::client::legacy::connect::http] connecting to [::1]:8888
[2024-06-12T09:58:12Z DEBUG hyper_util::client::legacy::connect::http] connected to [::1]:8888
[2024-06-12T09:58:13Z DEBUG hyper_util::client::legacy::pool] pooling idle connection for ("http", localhost:8888)
[2024-06-12T09:58:13Z DEBUG try_again] retry_async; retry_strategy=Retry { max_tries: 5, delay: Some(Static { delay: 1s }) } delay_strategy=TokioSleep
[2024-06-12T09:58:13Z DEBUG reqwest::connect] starting new connection: http://localhost:8888/
[2024-06-12T09:58:13Z DEBUG hyper_util::client::legacy::connect::dns] resolving host="localhost"
[2024-06-12T09:58:13Z DEBUG hyper_util::client::legacy::connect::http] connecting to [::1]:8888
[2024-06-12T09:58:13Z DEBUG hyper_util::client::legacy::connect::http] connected to [::1]:8888
[2024-06-12T09:58:13Z DEBUG hyper_util::client::legacy::pool] pooling idle connection for ("http", localhost:8888)
[2024-06-12T09:58:13Z INFO axum_keycloak_auth::instance] Received new jwk_set containing 2 keys.
[...]

Here you should/could see a error message, if your application is not able to receive the public key from your keycloak server.

The above example is just a example. You don't have to use fern. I'm pretty sure you can use any crate listed here https://crates.io/crates/log under "In executables"

elliot-ast commented 2 months ago

Alright great thanks for the help on logging! So the reason seems to be because of self signed certificates as this seems to be what is causing the response

[2024-06-12T10:21:26Z INFO axum_keycloak_auth::instance] Starting OIDC discovery.
[2024-06-12T10:21:26Z DEBUG try_again] retry_async; retry_strategy=Retry { max_tries: 5, delay: Some(Static { delay: 1s }) } delay_strategy=TokioSleep
[2024-06-12T10:21:26Z DEBUG reqwest::connect] starting new connection: https://localhost:8443/
[2024-06-12T10:21:26Z DEBUG h2::codec::framed_read] received frame=Settings { flags: (0x1: ACK) }
[2024-06-12T10:21:26Z DEBUG h2::proto::settings] received settings ACK; applying Settings { flags: (0x0), max_concurrent_streams: 200, initial_window_size: 1048576, max_frame_size: 16384, max_header_list_size: 16384 }
[2024-06-12T10:21:26Z DEBUG hyper::client::connect::dns] resolving host="localhost"
[2024-06-12T10:21:26Z DEBUG hyper::client::connect::http] connecting to 127.0.0.1:8443
[2024-06-12T10:21:26Z DEBUG hyper::client::connect::http] connected to 127.0.0.1:8443
[2024-06-12T10:21:26Z DEBUG try_again] Operation was not successful. Waiting... tries=1 delay=1s
[2024-06-12T10:21:27Z DEBUG reqwest::connect] starting new connection: https://localhost:8443/
[2024-06-12T10:21:27Z DEBUG hyper::client::connect::dns] resolving host="localhost"
[2024-06-12T10:21:27Z DEBUG hyper::client::connect::http] connecting to 127.0.0.1:8443
[2024-06-12T10:21:27Z DEBUG hyper::client::connect::http] connected to 127.0.0.1:8443
[2024-06-12T10:21:27Z DEBUG try_again] Operation was not successful. Waiting... tries=2 delay=1s
[2024-06-12T10:21:28Z DEBUG reqwest::connect] starting new connection: https://localhost:8443/
[2024-06-12T10:21:28Z DEBUG hyper::client::connect::dns] resolving host="localhost"
[2024-06-12T10:21:28Z DEBUG hyper::client::connect::http] connecting to 127.0.0.1:8443
[2024-06-12T10:21:28Z DEBUG hyper::client::connect::http] connected to 127.0.0.1:8443
[2024-06-12T10:21:28Z DEBUG try_again] Operation was not successful. Waiting... tries=3 delay=1s
[2024-06-12T10:21:29Z DEBUG reqwest::connect] starting new connection: https://localhost:8443/
[2024-06-12T10:21:29Z DEBUG hyper::client::connect::dns] resolving host="localhost"
[2024-06-12T10:21:29Z DEBUG hyper::client::connect::http] connecting to 127.0.0.1:8443
[2024-06-12T10:21:29Z DEBUG hyper::client::connect::http] connected to 127.0.0.1:8443
[2024-06-12T10:21:29Z DEBUG try_again] Operation was not successful. Waiting... tries=4 delay=1s
[2024-06-12T10:21:30Z DEBUG reqwest::connect] starting new connection: https://localhost:8443/
[2024-06-12T10:21:30Z DEBUG hyper::client::connect::dns] resolving host="localhost"
[2024-06-12T10:21:30Z DEBUG hyper::client::connect::http] connecting to 127.0.0.1:8443
[2024-06-12T10:21:30Z DEBUG hyper::client::connect::http] connected to 127.0.0.1:8443

[2024-06-12T10:21:30Z ERROR try_again] Operation was not successful after maximum retries. Aborting with last output seen. tries=5 last_output=Err(OidcDiscovery { source: Send { source: reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(8443), path: "/realms/myapi/.well-known/openid-configuration", query: None, fragment: None }, source: hyper::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "../ssl/statem/statem_clnt.c", line: 1883 }]))) }, X509VerifyResult { code: 18, error: "self-signed certificate" })) } } })

[2024-06-12T10:21:30Z ERROR axum_keycloak_auth::instance] Could not retrieve OIDC config. err="Could not discover OIDC configuration.\n\nCaused by these errors (recent errors listed first):\n  1: RequestError: Could not send request\n  2: error sending request for url (https://localhost:8443/realms/myapi/.well-known/openid-configuration) *\n  3: error trying to connect *\n  4: error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:../ssl/statem/statem_clnt.c:1883: (self-signed certificate)\n  5: error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:../ssl/statem/statem_clnt.c:1883:\n\nNOTE: Some redundant information has been removed from the lines marked with *. Set SNAFU_RAW_ERROR_MESSAGES=1 to disable this behavior.\n"
[2024-06-12T10:21:30Z DEBUG h2::codec::framed_write] send frame=Headers { stream_id: StreamId(1), flags: (0x4: END_HEADERS) }
[2024-06-12T10:21:30Z DEBUG h2::codec::framed_write] send frame=Data { stream_id: StreamId(1), flags: (0x1: END_STREAM) }
[2024-06-12T10:21:30Z DEBUG rustls::common_state] Sending warning alert CloseNotify
:Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("localhost")), port: Some(8443), path: "/realms/myapi/.well-known/openid-configuration", query: None, fragment: None }, source: hyper::Error(Connect, Ssl(Error { code: ErrorCode(1), cause: Some(Ssl(ErrorStack([Error { code: 167772294, library: "SSL routines", function: "tls_post_process_server_certificate", reason: "certificate verify failed", file: "../ssl/statem/statem_clnt.c", line: 1883 }]))) }, X509VerifyResult { code: 18, error: "self-signed certificate" })) } } })
lpotthast commented 2 months ago

Hmm, your issue shouldn't be related to the implementation of this library.

I have such a setup running and working fine. I would guess that it's simply related to how the certificates are created / set up. Two things come to mind: