サイボウズ Office を Single Sign On 対応する

# 概要

パッケージ版の[サイボウズ Office](http://products.cybozu.co.jp/office/) は、LDAP 連携も SAML 連携にも対応していません。予算が確保できるのであれば、クラウド版のサイボウズ Office [^1]か[サイボウズガルーン](https://sites.google.com/a/sateraito.jp/sateraito-dounyuu/Home/sso/cybozugaroon)を使うべきだと思いますが、組織規模も予算規模も小さいため Single Sign On (SSO) 対応の予算は確保できませんでした。仕方がないので、リバースプロキシ方式で SSO 対応する Perl スクリプトを作成しました。

1. フロントエンドの Apache に、リバースプロキシとして動作する CGI を設置する。ただし、このリバースプロキシ CGI は、バックエンドで動作しているサイボウズ本来の CGI からの応答を監視していて、必要に応じて代理ログインを行う。
2. バックエンドの Apache で、サイボウズ本来の CGI を動作させる。
3. 画像などの静的なコンテンツは、フロントエンド側の Apache で処理する。

なお、本稿はサイボウズ Office 8 で動作確認していますので、最新版などをご利用の場合は適宜に読みかえて下さい。

[^1]: クラウド版のサイボウズ Office は、[SAML 連携に対応](https://help.cybozu.com/ja/general/admin/saml_settings.html)しているようです。

# 前提

* サイボウズ Office のアカウント名と、認証連携先のアカウント名が同一である
* サイボウズ Office のパスワードは、全員共通の秘密パスワードにしても差し支えない

# 手順

## フロントエンドの Apache にサイボウズ Office をインストールする

インストールマニュアルにしたがって、通常通りにサイボウズ Office が動作するように設定してください。デフォルトの設定では、以下の URL でアクセスできる場所にインストールされているはずだと思います。

* サイボウズの CGI ⇒ https://example.jp/cgi-bin/cbag/ag.cgi
* サイボウズの画像など静的なファイル ⇒ https://example.jp/cb80/

## バックエンドの Apache でもサイボウズ Office を動くように設定する

フロントエンドの設定をコピーして、https://example.jp/ で参照できるウェブページが https://example.jp:8443/ でも参照できるように設定してください。Debian GNU/Linux の場合は、以下のような手順になります。

```
cp -p /etc/apache2/sites-available/default-ssl.conf /etc/apache2/sites-available/my-ssl-backend.conf
vi /etc/apache2/sites-available/my-ssl-backend.conf
a2ensite my-ssl-backend
```

a2ensite する前に、/etc/apache2/sites-available/my-ssl-backend.conf の先頭のポート番号を以下のように書き換えておきます。

```apache
<IfModule mod_ssl.c>
        <VirtualHost _default_:8443>
                ServerName example.jp:8443
                ServerAdmin webmaster@example.jp
(以下略)
```

ここまでの設定で、https://example.jp:8443/cgi-bin/cbag/ag.cgi でもサイボウズ Office が動くようになっているはずですので、確認しておいてください。また、全ての既存ユーザのパスワードを、共通秘密パスワードに変更しておいてください。

## フロントエンドのサイボウズ Office を Shibboleth 認証管理下に置く

以下の設定を適切な箇所に記述します。

```apache
<LocationMatch "/(cgi-bin/cbag|cb80)/">
	AuthType shibboleth
	Require shibboleth
</LocationMatch>
```

これで、https://example.jp/cgi-bin/cbag/ag.cgi にたどり着く前に Shibboleth 認証が必要になっているはずですので、確認しておいてください。

なお、後述するリバースプロキシ CGI は REMOTE_USER 環境変数を参照しているだけですから、Shibboleth に限らず Apache の認証モジュールなら大抵は利用することができます。

## フロントエンドのサイボウズ Office をリバースプロキシ CGI で置き換える

まず、[mod_fcgid](http://httpd.apache.org/mod_fcgid/) と必要な perl モジュールをインストールしておきます。

```
apt-get install libapache2-mod-fcgid libfcgi-perl
```

次に、以下のような CGI を適当なディレクトリに配置します。なお、```$BACKENDURL``` は実際のバックエンド側 Apache で動作しているサイボウズ Office の URL に、また、```$PASSWORD``` はサイボウズ Office にログインする場合の共通秘密パスワードに修正してください。

```pl:cybozu-proxy.fcgi
#!/usr/bin/perl

use FCGI;
use LWP::UserAgent;
use strict;
use warnings;
binmode STDIN, ':bytes';
binmode STDOUT, ':bytes';
binmode STDERR, ':bytes';

our $BACKENDURL = 'https://example.jp:8443/cgi-bin/cbag/ag.cgi';
our $PASSWORD = '0123456789';

&run_proxy_server();

sub run_proxy_server {
    my $fcgi = FCGI::Request();
    my $ua = LWP::UserAgent->new( keep_alive => 1, max_redirect => 0 );
    while( $fcgi->Accept() >= 0 ){
        my $request = &create_request();
        $ua->agent( $ENV{'HTTP_USER_AGENT'} || 'libwww-perl' );
        my $response = $ua->simple_request( $request );
        unless( $response->header( 'X-CybozuLogin' ) ){
            &print_response( $response );
        } else {
            &login_cybozu( $ua );
        }
    }
}

sub login_cybozu {
    my $ua = shift;
    my $response = $ua->post( $BACKENDURL,
                              { _Account    => $ENV{'REMOTE_USER'},
                                Password    => $PASSWORD,
                                _System     => 'login',
                                _Login      => 1,
                                csrf_ticket => '' } );
    my( @cookie ) = $response->header( 'Set-Cookie' );
    if( grep( /\AAGSESSID=/, @cookie ) ){
        print "Status: 302\n";
        for my $x ( @cookie ) {
            print 'Set-Cookie: ', $x, "\n";
        }
        printf( "Location: %s?%s\n\n", $ENV{'SCRIPT_URI'}, $ENV{'QUERY_STRING'} );
    } else {
        printf <<'__forbidden__';
Status: 403 Forbidden
Content-Type: text/plain

You are not allowed to access Cybozu. Contact the administrator.
__forbidden__
    }
}

sub create_request {
    my $url = $BACKENDURL;
    $url .= $ENV{'PATH_INFO'} if $ENV{'PATH_INFO'};
    $url .= '?' . $ENV{'QUERY_STRING'} if $ENV{'QUERY_STRING'};
    my $request = HTTP::Request->new( $ENV{'REQUEST_METHOD'}, $url );
    while( my( $key, $val ) = each %ENV ){
        next unless $key =~ s/\AHTTP_//;
        next if $key =~ m/\A(HOST|CONNECTION)\Z/;
        $request->header( $key, $val );
    }
    if( my $type = $ENV{'CONTENT_TYPE'} ){
        $request->header('content-type', $type );
        if ( my $len = $ENV{'CONTENT_LENGTH'} ) {
            my $buf;
            read( STDIN, $buf, $len );
            $request->content( $buf );
        } else {
            $request->content( join( '', <STDIN> ) );
        }
    }
    $request->header( 'X-Forwarded-For', $ENV{REMOTE_ADDR} );
    $request->header( 'X-Forwarded-Host', $ENV{HTTP_HOST} );
    $request->header( 'X-Forwarded-Server', $ENV{SERVER_NAME} );
    $request;
}

sub print_response {
    my( $response ) = @_;
    my $status = $response->status_line;
    $status =~ s!\AHTTP/\d+\.\d+\s+!!;
    print 'Status: ', $status, "\n", $response->headers_as_string, "\n", $response->content;
}
```

この CGI は、以下のように動作します。

1. mod_fcgid 経由で、ブラウザからのリクエストを受け取る。
2. 受け取ったリクエスト(cookie などのヘッダおよびコンテンツ)に基づいて、バックエンドで動作しているサイボウズ Office 本来の CGI にアクセスするためのリクエストを作成する(create_request 関数)。
3. 得られたリクエストを用いて、バックエンドにアクセスする。
4. バックエンドからの応答において X-Cybozu-Login というヘッダに値がセットされていれば、サイボウズ Office へのログインができていない状態と判定し、代理ログインするための login_cybozu 関数に遷移。それ以外の場合は、バックエンドからの応答をそのまま出力する(リバースプロキシとして動作する)。

その上で，フロントエンドのサイボウズ Office の代わりに、上記のリバースプロキシ CGI が呼び出されるように以下の設定を書いてください。

```apache
ScriptAlias /cgi-bin/cbag/ag.cgi /local/example/cbag/cybozu-proxy.fcgi
```

これで、https://example.jp/cgi-bin/cbag/ag.cgi にアクセスすると、リバースプロキシ用 CGI によって代理ログインされて、サイボウズ Office が使えるはずです。

## バックエンドのサイボウズ Office へのアクセスを制限する

サイボウズ Office の全ユーザが同一の共通秘密パスワードを使うようになっていますので、バックエンドのサイボウズ Office に直接アクセスされると、他ユーザへのなりすましが可能です。この問題を避けるために、バックエンドの Apache には以下のように設定して、外部からのアクセスを禁止しておきましょう。

```apache
<LocationMatch "/(cgi-bin/cbag|cb80)/">
	Order deny,allow
	Deny from all
	Allow from aaa.bbb.ccc.ddd
</LocationMatch>
```

```aaa.bbb.ccc.ddd``` は、サーバの IP アドレスで置き換えてください。

# 注意点

ユーザーが、サイボウズ Office の「個人設定」で自分のパスワードを変更してしまうと、login_cybozu 関数による代理ログインが動作しないようになってしまいます。「システム設定」→「ユーザー管理」→「パスワードの制限」と進んで、「個人設定でのパスワード変更」を「許可しない」に設定しておきましょう。