2019年4月12日金曜日

Webアプリのユーザ認証でsaslauthdを利用してみる

まあ、別にWebアプリじゃなくてもいいんですけど、ユーザー名とパスワードが正しいかを判定する仕組みを実装するのって結構面倒くさいじゃないですか。

あるいは、すでにログインアカウント名に使われているのと同じアカウント名とパスワードを使いまわしている場合に、自分のアプリケーションでは設定を許さずに違ったパスワードじゃないとエラーにしたいとか。こういったケースでも入力されたユーザ名とパスワードが使われているかどうかの判定が必要になります。

ですが、アプリから/etc/passwdやら/etc/shadowを直接参照するなんて背筋が凍ります。それに、これ以外の認証機構にはまた別のコードを書かなくちゃなりません。

そこで、saslauthdにお願いして、ユーザ名とパスワードが正しいかを判定してもらっちゃおうというのが本記事の目論見です。

saslauthdの説明はそれこそweb上で検索すれば山ほど情報が出てきますが、実際にはほとんどpostfix+dovecotの設定の仕方ばっかりで意外に思われるかもしれませんが、postfixやdovecot以外からでも簡単に使えるんです。

使い方は単純。testsaslauthdコマンドだけでOK。
戻り値が0だったら認証が通ったという単純明快さです。
C言語で各種APIを使ったライブラリを作る必要もありません。

・・・と、言いたいところですが、困ったことにtestsaslauthdコマンドはパスワードを引数で指定しなければなりません。
これだと手動で打ったらシェルのhistoryにばっちり残ってしまいますし、psコマンドでパスワードが見えちゃいます。
極めて遺憾かつ慚愧の念に堪えません。

ですので、別の方法を採用したいと思います。
といっても、これまた単純明快です。

実は、saslauthdが用意してくれているunixドメインソケットにユーザ名とパスワードとサービス名とrealm名をのっけた電文を突っ込むだけで、そのユーザ名とパスワードが正当なものか判定した結果を返してくれます。
testsaslauthdコマンドも、実際にはこの方法を使って実装されています。

認証依頼電文そのものも超シンプル。
フォーマットは、
ユーザ名文字列長(ネットワークバイトオーダーでWORD)
ユーザ名文字列
パスワード文字列長(ネットワークバイトオーダーでWORD)
パスワード文字列
サービス名文字列長(ネットワークバイトオーダーでWORD)
サービス名文字列
realm名文字列長(ネットワークバイトオーダーでWORD)
realm名文字列
というシンプルさです。

結果応答電文も素っ気ないほどシンプル。
ユーザ名とパスワードが正しかった場合:
\000\002("OK"の長さ。ネットワークバイトオーダーでWORD)
OK(文字列。2byte)
だけ。正しくない場合はOKの代わりにNOとなり、その後に理由文字列がついてきます。

こんなに簡単なのだから利用しない手はありません。
そこで、webアプリと言えばみんな大好き(私は嫌い)なphpでユーザ認証関数を作ると以下のような感じになります。
関数の使い方は・・・見ればわかりますよね。例によってエラーチェックが長いですが、Saslauthd::auth()からtrueが返ってきたらユーザ名とパスワードが認証を通った、というだけの関数です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php
class Saslauthd
{
  static public function auth( $user, $pass, $service_name = "password-auth", $realm = "", $socket_name = "/run/saslauthd/mux", &$errorMessage = null )
  {
    if( $user === null || $user == "" || strlen( $user ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid user";
      }
      return false;
    }
    if( $pass === null || strlen( $pass ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid password";
      }
      return false;
    }
    if( $socket_name === null || $socket_name === "" ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid socket name";
      }
      return false;
    }
    if( $service_name === null || $service_name == "" || strlen( $service_name ) > 100 ){
      if( $errorMessage !== null ){
        $errorMessage = "invalid service name";
      }
      return false;
    }
     
    if( $realm === null || strlen( $realm ) > 100){
      if( $errorMessage !== null ){
        $errorMessage = "invalid realm";
      }
      return false;
    }
 
    $sock = socket_create( AF_UNIX, SOCK_STREAM, 0 );
    if( $sock === false ){
      if( $errorMessage !== null ){
        $errorMessage = socket_strerror( socket_last_error() );
      }
      return false;
    }
     
    $ret = socket_connect( $sock, $socket_name );
    if( $ret === false ){
      if( $errorMessage !== null ){
        $errorMessage = socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
     
    $sendbuf = "";
     
    $sendbuf .= pack("n", strlen( $user )). $user ;
    $sendbuf .= pack("n", strlen( $pass )). $pass ;
    $sendbuf .= pack("n", strlen( $service_name )).$service_name;
    $sendbuf .= pack("n", strlen( $realm )).$realm;
 
    $ret = self::sockwrite( $sock, $sendbuf );
     
    if( true !== $ret ){
      socket_close( $sock );
      if( $errorMessage !== null ){
        $errorMessage = "socket_write error: ".$ret;
      }
      return false;
    }
    $readlenstr = socket_read( $sock, 2, PHP_BINARY_READ );
    if( $readlenstr === false ){
      if( $errorMessage !== null ){
        $errorMessage = "socket_read error: ".socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
    $readlen = unpack( "n", $readlenstr )[1];
    $ret = socket_read( $sock, $readlen, PHP_BINARY_READ );
    if( $ret === false ){
      if( $errorMessage !== null ){
        $errorMessage = "socket_read error: ".socket_strerror( socket_last_error() );
      }
      socket_close( $sock );
      return false;
    }
    socket_close( $sock );
     
    if( $ret === "OK" ){
      return true;
    }
    if( $errorMessage !== null ){
      $errorMessage = $ret;
    }
    return false;
  }
  static private function sockwrite( $sock, $buf )
  {
    $spos = 0;
    $buflen = strlen( $buf );
    if( $buflen === 0 ){
      return true;
    }
    while( true ){
      $ret = socket_write( $sock, substr( $buf, $spos ) );
      if( $ret === false ){
        return socket_strerror( socket_last_error() );
      }
      $spos += $ret;
      if( $spos <= $buflen ){
        break;
      }
    }
    return true;
  }
}
?>
パスワードをhttpsを使わないで通信して全世界に公開したり連続飽和攻撃を考慮しないプログラムを作ったりして頭を抱えるようなことがありませんようにご注意ください。

お読みいただいてありがとうございました。

0 件のコメント:

コメントを投稿