FHEMWEB basicAuth gegen einen Radius-Server

Begonnen von betateilchen, 23 Dezember 2025, 13:06:53

Vorheriges Thema - Nächstes Thema

betateilchen

defmod web2 FHEMWEB 8085 global

defmod allowed4web2 allowed
attr allowed4web2 basicAuth {radius($user,$password)}
attr allowed4web2 validFor web2

sub radius {
  use Authen::Radius; #apt install libauthen-radius-perl
  my ($user,$password) = @_;
  my $clientPassword = getKeyValue("clientPassword");
  my $r = new Authen::Radius(Host => '192.168.123.152', Secret => $clientPassword);
  return $r->check_pwd($user,$password);
}

  • 192.168.123.152 ist die IP Adresse des Radius Servers
  • das Client-Password wurde mit setKeyValue("clientPassword","geheimesPassword") in den FHEM-keystore abgelegt

Warum? Weil in meinem Netzwerk eh ein Radius Server läuft.
Mich hatte einfach interessiert, ob das funktioniert und wie aufwändig es wird.
Dank der perl Library Authen::Radius ist das Ganze aber sehr einfach umzusetzen.

-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

JudgeDredd

Hallo betateilchen,

ja ist denn heute schon Weihnachten ? 😃
Die Idee ist echt prima, das werde ich über die Feiertage definitiv mal ausprobieren.

Ich hatte vor vielen Weihnachten mal das LDAP-Modul (keine Ahnung ob das im SVN ist) ausprobiert, aber dazu hätte ich Änderungen am LDAP-Schema vornehmen müssen, an die ich nicht ran wollte.

So kann ich nun die Auth gegen mein AD machen.

hab Dank für Deinen Vorschlag und ein frohes Fest,
JudgeDredd
Router: Eigenbau (pfSense)
FHEM: Proxmox (DELL R720) | Debian 12 (VM)

CoolTux

#2
Schöne Lösung. Auch nice wäre ein OAuth2 oder besser gleich OIDC. Mein Keycloak würde sich da freuen   ;D
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

betateilchen

Beschreibe mir doch mal das Szenario, das Dir da vorschwebt.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

CoolTux

So ganz allgemein. So wie bisher BasicAuth funktioniert nur eben per SSO mittels OIDC. In meinem Fall an Keycloak.
Wenn der Token abgelaufen ist wird an Keycloak weiter geleitet um sich zu Authentifizieren.
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

betateilchen

Also im Prinzip sowas ähnliches, wie wir es bei der Anmeldung mit einem OTP gegen den Google-Authenticator bereits haben? Wenn die Anmeldung erfolgreich war, wird das für maximal 86400 Sekunden gecached und nicht mehr neu gefragt. Danach muss man sich neu anmelden.

Das sollte doch eigentlich nicht allzu schwierig umzusetzen sein. Das allowed-device muss ja nur einmal ein "true" zurückbekommen und sich das merken.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

passibe

Zitat von: CoolTux am 23 Dezember 2025, 16:37:46Schöne Lösung. Auch nice wäre ein OAuth2 oder besser gleich OIDC. Mein Keycloak würde sich da freuen   ;D
Ich nutze oauth2-proxy und nginx auth_request. Nginx sendet dann, bei erfolgreicher Anmeldung über den oauth2-proxy, einfach den Basic Auth header mit (also user:pass in base64; ähnlich wie in diesem Beispiel X-Access-Token nur eben hardcoded). In FHEM muss man also gar nichts umstellen.

oauth2-proxy funktioniert auch mit Keycloak als IDP. (Ich nutze aktuell Nextcloud als IDP, weil das hier eh läuft.)

CoolTux

Mein FHEM läuft in einem Kubernetes Cluster und ist über Traefik Ingress aufrufbar. Muss mal schauen ob man Traefik diesbezüglich konfigurieren kann.
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net


betateilchen

Zitat von: CoolTux am 23 Dezember 2025, 16:37:46Schöne Lösung. Auch nice wäre ein OAuth2 oder besser gleich OIDC. Mein Keycloak würde sich da freuen   ;D

Es gibt doch für keycloak ein radius-plugin.
Würde das Deine Aufgabenstellung nicht abdecken?

(von keycloak habe ich keine Ahnung, ich habe nur ein bisschen im Internet rumgelesen, nachdem Du mit Deinem Wunsch kamst)
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

CoolTux

Zitat von: betateilchen am 26 Dezember 2025, 11:23:11
Zitat von: CoolTux am 23 Dezember 2025, 16:37:46Schöne Lösung. Auch nice wäre ein OAuth2 oder besser gleich OIDC. Mein Keycloak würde sich da freuen   ;D

Es gibt doch für keycloak ein radius-plugin.
Würde das Deine Aufgabenstellung nicht abdecken?

(von keycloak habe ich keine Ahnung, ich habe nur ein bisschen im Internet rumgelesen, nachdem Du mit Deinem Wunsch kamst)

Das muss ich mir mal in Ruhe anschauen. Wird sicher nicht Standard sein sondern muss man als Zusatz einbinden. Danke Dir auf jeden Fall.
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

betateilchen

-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

JudgeDredd

So, die Feiertage sind vorbei und ich habe mich mit der RADIUS-Auth mal beschäftigt.
Die erste Euphorie war allerdings schnell verflogen, da mein RADIUS nicht für PlainText-Passwort (PAP) konfiguriert ist.
Die Begeisterung für die Idee von betateilchen war aber ungebrochen.
Daher habe ich mir eine Sub für die RADIUS-Auth mittels MSCHAPv2 gebaut.
Die Response-Challenge ist hier manuell nachgebaut, da es dafür kein debian Paket gibt und ich von cpan absehen wollte.

Vielleicht gibt es ja weitere User, die ebenfalls MSCHAPv2 nutzen und Interesse daran haben.
use Authen::Radius;       #Debian Paket libauthen-radius-perl
use Digest::MD4 qw(md4);  #Debian Paket libdigest-md4-perl
use Digest::SHA qw(sha1); #      "            "
use Crypt::DES;           #Debian Paket libcrypt-des-perl

sub radius_mschapv2($$) {

    # -----------------------------
    # Konfiguration
    # -----------------------------
    my ($username, $password ) = @_;
    Log3 undef, 4, "Starting Auth for $username";

    my $radius_server            = 'radius.mydomain.intranet';                #IP/FQDN of RADIUS
    my $radius_secret            = getKeyValue('RADIUSclientPassword') || ''; #Client Password (clients.conf) keepair set via setKeyValue('RADIUSclientPassword','password')
    my $radius_port              = 1812;                                      #Listen Port
    my $radius_nas_Identifier    = 'fhem-server';                             #optional Attribut

    # -----------------------------
    # MS-CHAPv2 Parameter
    # -----------------------------
    my $auth_challenge = random_bytes(16);  # AuthenticatorChallenge
    my $peer_challenge = random_bytes(16);  # PeerChallenge

    # NT-Password-Hash
    my $nt_hash = md4(encode_unicode($password));

    # MS-CHAPv2 Response berechnen
    my $nt_response = mschapv2_response(
        $peer_challenge,
        $auth_challenge,
        $username,
        $nt_hash
    );

    Authen::Radius->load_dictionary( '/usr/share/freeradius/dictionary' );

    # -----------------------------
    # RADIUS Initialisierung
    # -----------------------------
    my $radius = Authen::Radius->new(
        Host  => $radius_server,
        Secret => $radius_secret,
        Port  => $radius_port,
        TimeOut => 5,
    );
    $radius->clear_attributes;

    # -----------------------------
    # RADIUS Attribute setzen
    # -----------------------------
    $radius->add_attributes(
        { Name => 'Service-Type',      Value => 'Login-User' },
        { Name => 'User-Name',         Value => $username },
        { Name => 'MS-CHAP-Challenge', Value => $auth_challenge },
        { Name => 'MS-CHAP2-Response', Value => pack("C C a24 a24", 1, 0, $peer_challenge, $nt_response ), Type => 'octets' },
        { Name => 'NAS-Identifier',    Value => $radius_nas_Identifier },
    );

    # -----------------------------
    # Access-Request senden
    # -----------------------------
    $radius->send_packet(ACCESS_REQUEST) and my $res = $radius->recv_packet();

    # -----------------------------
    # Ergebnis
    # -----------------------------

    if ( defined($res) ) {
        if ($res == ACCESS_ACCEPT) {
            Log3 undef, 4, "Auth for $username successfull";
            return $res;
        } elsif ($res == ACCESS_REJECT) {
            Log3 undef, 4, "Auth for $username failed";
        } else {
            Log3 undef, 4, "Auth for $username unknown ($res)";
        }
    } else {
        Log3 undef, 4, "No reply from $radius_server"
    }

    # =========================================================
    # Hilfsfunktionen
    # =========================================================

    sub random_bytes {
        my ($len) = @_;
        return join '', map { chr(int(rand(256))) } 1 .. $len;
    }

    sub encode_unicode {
        my ($str) = @_;
        return join('', map { $_ . "\x00" } split //, $str);
    }

    sub mschapv2_response {
        my ($peer_challenge, $auth_challenge, $user, $nt_hash) = @_;

        # ChallengeHash = SHA1(PeerChallenge | AuthChallenge | Username)[0..7]
        my $challenge = substr(
            sha1($peer_challenge . $auth_challenge . $user),
            0, 8
        );

        return des_encrypt_challenge($challenge, $nt_hash);
    }

    sub des_encrypt_challenge {
        my ($challenge, $hash) = @_;

        my $response = '';

        for my $i (0..2) {
            my $key = substr($hash . ("\0" x 5), $i * 7, 7);
            $key = expand_des_key($key);

            my $des = Crypt::DES->new($key);
            $response .= $des->encrypt($challenge);
        }

        return $response;
    }

    sub expand_des_key {
        my ($key7) = @_;
        my @k = unpack("C7", $key7);
        my @key8;

        $key8[0] = $k[0] & 0xFE;
        $key8[1] = (($k[0] << 7) | ($k[1] >> 1)) & 0xFE;
        $key8[2] = (($k[1] << 6) | ($k[2] >> 2)) & 0xFE;
        $key8[3] = (($k[2] << 5) | ($k[3] >> 3)) & 0xFE;
        $key8[4] = (($k[3] << 4) | ($k[4] >> 4)) & 0xFE;
        $key8[5] = (($k[4] << 3) | ($k[5] >> 5)) & 0xFE;
        $key8[6] = (($k[5] << 2) | ($k[6] >> 6)) & 0xFE;
        $key8[7] = ($k[6] << 1) & 0xFE;

        return pack("C8", @key8);
    }
}
Router: Eigenbau (pfSense)
FHEM: Proxmox (DELL R720) | Debian 12 (VM)