Skip to content

認証(Google Workspace の SAML SSO / SMTP の OAuth) ​

第10版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

公式マニュアルでは Microsoft 365 向けの手順が中心の、Google Workspace(以下 GWS)との認証連携をまとめます。

  • SAML SSO: GWS で「カスタム SAML アプリ」を作り、取得した IdP 情報を Authentication.json に設定します(1.4 系以降が対象)。
  • SMTP の OAuth: 1.5.1.0 から SMTP 認証に OAuth 2.0 が使えます。GWS ではサービスアカウントと「ドメイン全体の委任」を組み合わせ、Mail.json を設定します。トークンの取得・キャッシュの仕組みと、Microsoft 365 での設定例も載せています。

INFO

以下の URL・ID・メールアドレスはすべてダミー値です。pleasanter.example.com などは実際の環境の値に置き換えてください。

SAML SSO 1.4 以降 ​

仕組みと設定の全体像 ​

プリザンターが SP(Service Provider)、GWS が IdP(Identity Provider)として動作します。

図を読み込み中…

設定ファイルは Implem.Pleasanter/App_Data/Parameters/Authentication.json です。大きく 4 つのブロックで構成されます。

ブロック設定内容
ルート設定Provider を "SAML" にして SAML 認証を有効化
SPOptionsプリザンター(SP)自身のエンティティ ID やコールバック URL
IdentityProvidersGWS(IdP)のエンティティ ID、SSO URL、証明書情報
AttributesSAML 属性とプリザンターのユーザー項目のマッピング

作業の順番は次のとおりです。

  1. GWS 管理コンソールでカスタム SAML アプリを作成し、IdP 情報(エンティティ ID・SSO URL・証明書)を取得する
  2. 取得した IdP 情報を Authentication.json に設定する
  3. プリザンターに設定を反映する(パラメータリロードまたは再起動)
  4. 動作確認する

GWS 管理コンソールでの設定 ​

1. カスタム SAML アプリの追加 ​

  1. Google 管理コンソール にログインする
  2. 「アプリ」>「ウェブアプリとモバイルアプリ」を選択する
  3. 「アプリを追加」>「カスタム SAML アプリの追加」をクリックする
  4. アプリ名に「Pleasanter」と入力して「続行」

2. IdP 情報の取得 ​

「Google IdP 情報」画面で次の値を控えます。

項目例用途
SSO の URLhttps://accounts.google.com/o/saml2/idp?idpid=xxxxxAuthentication.json の SignOnUrl
エンティティ IDhttps://accounts.google.com/o/saml2?idpid=xxxxxAuthentication.json の EntityId(IdP)
証明書X.509 証明書(PEM 形式)プリザンターサーバーの証明書ストアにインポート

WARNING

証明書は「ダウンロード」ボタンで .pem ファイルとして保存しておきます。プリザンター側の設定で必要になります。

3. サービスプロバイダ(SP)情報の入力 ​

項目設定値説明
ACS の URLhttps://pleasanter.example.com/Saml2/AcsIdP が SAML レスポンスを POST する URL
エンティティ IDhttps://pleasanter.example.com/Saml2プリザンター(SP)のエンティティ ID
名前 ID の形式EMAILメールアドレスを NameID として使用

ACS の URL は /Saml2/Acs です。プリザンターは Sustainsys.Saml2 の AddSaml2 で SAML を組み込んでおり(Startup.cs)、SetSPOptions では ModulePath を変えていません(Saml.cs)。そのため SAML レスポンスを受け取るのは Sustainsys.Saml2 の既定のモジュールパス /Saml2 配下の /Saml2/Acs です。/Users/SamlLogin は、ACS で認証が済んだ後にリダイレクトされるプリザンターのアクション(UsersController.cs)で、SAML レスポンスを処理しません。ここを ACS の URL に登録すると、ログインが完了しません。

4. 属性マッピング ​

GWS の標準ディレクトリ属性には「部門名(Department)」はありますが「部門コード」はありません。部門コードなどは カスタム属性 を作って管理します。

カスタム属性は「ディレクトリ」>「ディレクトリ設定」>「カスタム属性」>「カスタム属性を追加」(またはユーザー編集画面の「その他のユーザー情報の追加…」)から作成します。Pleasanter のようなカテゴリにまとめておくと管理しやすくなります。

カスタム フィールド名情報の種類用途
DeptCodeテキストプリザンターの部門コード
UserCodeテキストプリザンターのユーザーコード
TenantManagerテキストテナント管理者フラグ(true/false)

いずれも「ユーザーや管理者に表示: はい」「値の数: 1 つの値」で作成します。各ユーザーの値は管理コンソールのユーザー編集画面か、Google Directory API で一括設定できます。

カスタム SAML アプリの属性マッピングには次をすべて設定します。

Google のディレクトリ属性アプリの属性
Primary emailMailAddress
First nameFirstName
Last nameLastName
DepartmentDept
Pleasanter - DeptCodeDeptCode
Pleasanter - UserCodeUserCode
Pleasanter - TenantManagerTenantManager

INFO

カスタム属性は「カテゴリ名 - フィールド名」の形式で表示されます(上の表はカテゴリ名を Pleasanter にした場合)。

5. ユーザーアクセスの有効化 ​

作成したアプリは既定で オフ です。アプリ画面の「ユーザー アクセス」で「オン(すべてのユーザー)」、または「オン(一部の組織部門)」で対象の組織部門を選んで保存します。

証明書のインポート ​

GWS からダウンロードした .pem をプリザンターのサーバーにインポートし、拇印(Thumbprint)を控えます。

Windows ​

  1. .pem をダブルクリックして「証明書のインポートウィザード」を開く
  2. 保存場所は Authentication.json の StoreLocation に合わせる
    • CurrentUser: 現在のユーザー
    • LocalMachine: ローカル コンピューター(IIS で動かす場合はこちらを推奨)
  3. 証明書ストアは「個人」(My)を選ぶ
  4. certlm.msc または certmgr.msc でインポートした証明書の拇印をコピーする

Linux ​

.NET の X509Store が証明書を検索できる場所にインポートします。CA トラストストアへの追加だけでは不十分なケースがあるため、certutil -user -addstore My でプリザンターの実行ユーザーの個人ストアにもインポートしておくのが確実です。

bash
# Debian / Ubuntu 系
sudo cp GoogleIDPCertificate.pem /usr/local/share/ca-certificates/google-idp.crt
sudo update-ca-certificates
# プリザンターの実行ユーザーで実行
certutil -user -addstore My /usr/local/share/ca-certificates/google-idp.crt
bash
# RHEL / CentOS / AlmaLinux / Rocky Linux 系
sudo cp GoogleIDPCertificate.pem /etc/pki/ca-trust/source/anchors/google-idp.pem
sudo update-ca-trust
# プリザンターの実行ユーザーで実行
certutil -user -addstore My /etc/pki/ca-trust/source/anchors/google-idp.pem
項目Debian / UbuntuRHEL / CentOS / AlmaLinux
証明書の配置先/usr/local/share/ca-certificates//etc/pki/ca-trust/source/anchors/
拡張子.crt.pem
更新コマンドupdate-ca-certificatesupdate-ca-trust
certutil を含むパッケージlibnss3-toolsnss-tools

WARNING

certutil -user -addstore My は Windows の certutil の書式で、Linux の NSS の certutil の書式とは異なります。また .NET の Linux 版の X509Store が NSS のデータベースを参照するかは、プリザンターのソースからは確認できません。プリザンターの実行ユーザーで .NET の X509Store に直接追加する方法を 証明書のインポート にまとめています。

拇印はディストリビューションに関係なく次で確認できます。コロンを除いた文字列を FindValue に設定します。

bash
openssl x509 -in GoogleIDPCertificate.pem -fingerprint -sha1 -noout | sed 's/SHA1 Fingerprint=//;s/://g'

Azure App Service ​

  1. 対象の App Service の「証明書」メニューで「公開キー証明書 (.cer) の追加」を選び、.pem をアップロードする
  2. 一覧に表示された拇印をコピーする
  3. 「構成」→「アプリケーション設定」で WEBSITE_LOAD_CERTIFICATES にその拇印(すべて読み込む場合は *)を設定する
  4. 保存して App Service を再起動する

WARNING

WEBSITE_LOAD_CERTIFICATES を設定しないと、アップロードした証明書をアプリのプロセスから参照できません。

App Service(Windows)では、読み込まれた証明書は CurrentUser の My ストアに配置されるため、StoreLocation は CurrentUser にします。App Service(Linux)では /var/ssl/certs/<拇印>.der にファイルとして配置されますが、X509Store から使うには同様に WEBSITE_LOAD_CERTIFICATES の設定が必要です。

Authentication.json ​

json
{
    "Provider": "SAML",
    "SamlParameters": {
        "Attributes": {
            "FirstName": "FirstName",
            "LastName": "LastName",
            "DeptCode": "DeptCode",
            "Dept": "Dept",
            "UserCode": "UserCode",
            "TenantManager": "TenantManager",
            "MailAddress": "{NameId}"
        },
        "SamlTenantId": 1,
        "DisableOverwriteName": false,
        "SPOptions": {
            "EntityId": "https://pleasanter.example.com/Saml2",
            "ReturnUrl": "https://pleasanter.example.com/Users/SamlLogin",
            "AuthenticateRequestSigningBehavior": "IfIdpWantAuthnRequestsSigned",
            "OutboundSigningAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
            "MinIncomingSigningAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
        },
        "IdentityProviders": [
            {
                "EntityId": "https://accounts.google.com/o/saml2?idpid=xxxxx",
                "SignOnUrl": "https://accounts.google.com/o/saml2/idp?idpid=xxxxx",
                "LogoutUrl": null,
                "AllowUnsolicitedAuthnResponse": true,
                "Binding": "HttpPost",
                "WantAuthnRequestsSigned": false,
                "DisableOutboundLogoutRequests": true,
                "LoadMetadata": false,
                "SigningCertificate": {
                    "StoreName": "My",
                    "StoreLocation": "LocalMachine",
                    "X509FindType": "FindByThumbprint",
                    "FindValue": "A1B2C3D4E5F6..."
                }
            }
        ]
    }
}

Provider ​

設定値説明
null独自認証(既定)
"SAML"SAML 認証を有効化(シングルテナント)
"SAML-MultiTenant"SAML 認証を有効化(マルチテナント)

SPOptions / IdentityProviders ​

項目設定値
SPOptions.EntityIdGWS の SP 情報で入力したエンティティ ID(https://<ドメイン>/Saml2)と一致させる
SPOptions.ReturnUrlhttps://<ドメイン>/Users/SamlLogin。ACS(/Saml2/Acs)で認証した後の戻り先で、IdP 起点のログインなど戻り先の指定が無いときに使われる。GWS の ACS の URL とは別の値にする
IdentityProviders.EntityIdGWS の「エンティティ ID」
IdentityProviders.SignOnUrlGWS の「SSO の URL」
FindValueインポートした証明書の拇印
StoreLocation証明書をインポートした場所(CurrentUser / LocalMachine)

OutboundSigningAlgorithm と MinIncomingSigningAlgorithm には、XML 署名のアルゴリズム識別子 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256(http)を書きます。アクセス先の URL ではなく識別子なので https に変えません。プリザンター同梱の Authentication.json の既定値も http です(Authentication.json)。

属性(Attributes)の仕様 ​

指定できる属性 ​

属性キーマッピング先値の型説明
MailAddressMailAddresses テーブル文字列メールアドレス。{NameId} で NameID を使用可
NameUsers.Name文字列表示名(フルネーム)。FirstName/LastName より優先
FirstNameUsers.FirstName文字列名。Name 未設定時に使用
LastNameUsers.LastName文字列姓。Name 未設定時に使用
FirstAndLastNameOrderUsers.FirstAndLastNameOrder数値姓名の結合順。1 = 名→姓、2 = 姓→名
UserCodeUsers.UserCode文字列ユーザーコード
BirthdayUsers.Birthday日付誕生日(パース可能な日付文字列)
GenderUsers.Gender文字列性別
LanguageUsers.Language文字列言語コード(ja、en など)
TimeZoneUsers.TimeZone文字列タイムゾーン(Asia/Tokyo など)
TenantManagerUsers.TenantManager真偽値テナント管理者フラグ。true/false(大文字小文字不問)
DeptCodeDepts.DeptCode文字列部門コード。Dept とセットで指定
DeptDepts.DeptName文字列部門名。DeptCode とセットで指定
BodyUsers.Body文字列備考

MailAddress の指定方法は 3 通りです。

指定方法例動作
{NameId}"MailAddress": "{NameId}"SAML の NameID をメールアドレスとして使用
クレーム名"MailAddress": "mail"指定クレームの値を使用
フォールバック"MailAddress": "mail|{NameId}"指定クレームを探し、なければ NameID を使用

GWS で名前 ID の形式を EMAIL にしていれば、{NameId} にはユーザーのメールアドレスが入ります。

表示名の決まり方 ​

優先度条件表示名
1Name が設定されているName の値
2FirstName と LastName の両方FirstAndLastNameOrder に従って結合
3LastName のみLastName の値
4FirstName のみFirstName の値

FirstAndLastNameOrder の既定は姓→名です。DisableOverwriteName を true にすると、既存ユーザーの名前は SAML ログイン時に上書きされず、新規作成時のみ反映されます。

IdP が属性を送らなかったとき ​

属性の状態既存ユーザー新規ユーザー
IdP から値が送信されたIdP の値で上書きIdP の値で設定
IdP から値が送信されなかった現在の値を保持DB の既定値
Attributes に定義していない変更なしDB の既定値

属性に既定値を持たせたいとき ​

Attributes の値は IdP のクレーム名として解釈されるため、"Language": "ja" と書いても「ja というクレーム」を探すだけで、固定値にはなりません。

方法内容向いているケース
IdP 側で設定GWS のカスタム属性(Language、TimeZone など)に全ユーザー分の値を入れ、属性マッピングに追加する全ユーザーに同じ値を強制したい(IdP が正)
Attributes から除外定義自体を削除する。SAML ログイン時に更新されず、プリザンター側の値がそのまま使われるプリザンター側でユーザーごとに管理したい

属性ごとに組み合わせることもできます。GWS 側の一括設定は Admin SDK の Directory API などを使います。

bash
# gcloud CLI でカスタム属性を設定する例
gcloud identity users update user@example.com \
  --custom-schemas='Pleasanter={Language:ja,TimeZone:Asia/Tokyo}'

Service.json の既定値は使われない

Service.json の DefaultLanguage・TimeZoneDefault やテナント設定の言語・タイムゾーンは、SAML によるユーザー新規作成時には参照されません。SAML のユーザー作成処理は通常のユーザー作成パス(Controller → Model)を通らず、Saml.cs 内で直接 SQL を発行しているためです。言語やタイムゾーンを確実に設定するには、IdP 側から属性として送信します。

IdP から送られなかった列には DB の既定値が入ります。1.5.8.1 の列定義では Users.Language の既定値が ja、Users.TimeZone の既定値が Asia/Tokyo です(Users_Language.json、Users_TimeZone.json)。日本語・日本時間の環境ならそのままで困りません。

別の値にそろえたい場合は、SAML ユーザー作成後に SQL で更新します(手動実行、または定期ジョブ化が必要です)。新規作成直後のユーザーは空文字ではなく既定値(ja / Asia/Tokyo)を持つので、"Language" = '' のような条件では該当しません。次は既定値のままのユーザーを英語・UTC にする例です。

sql
-- 既定値のままのユーザーの言語・タイムゾーンを一括設定
UPDATE [Users]
SET
    [Language] = 'en',
    [TimeZone] = 'UTC'
WHERE [TenantId] = 1
    AND [Language] = 'ja'
    AND [TimeZone] = 'Asia/Tokyo'
    AND [TenantManager] = 0;
sql
-- 既定値のままのユーザーの言語・タイムゾーンを一括設定
UPDATE "Users"
SET
    "Language" = 'en',
    "TimeZone" = 'UTC'
WHERE "TenantId" = 1
    AND "Language" = 'ja'
    AND "TimeZone" = 'Asia/Tokyo'
    AND "TenantManager" = false;
sql
-- 既定値のままのユーザーの言語・タイムゾーンを一括設定
UPDATE `Users`
SET
    `Language` = 'en',
    `TimeZone` = 'UTC'
WHERE `TenantId` = 1
    AND `Language` = 'ja'
    AND `TimeZone` = 'Asia/Tokyo'
    AND `TenantManager` = 0;

部門・グループ・同期タイミング ​

部門(Depts)の自動管理 ​

SAML ログイン時に DeptCode と Dept の 両方 が送られてくると、次の処理が自動で行われます。

  1. DeptCode に一致する部門がなければ新規作成する
  2. あれば部門名を Dept の値で上書きする
  3. ユーザーをその部門に所属させる

図を読み込み中…

Dept だけで DeptCode を省略した場合、部門の自動管理は行われません。

WARNING

DeptCode を空文字列で送ると、ユーザーの部門所属が 解除 されます(DeptId = 0)。GWS 側でカスタム属性を空にした場合にこの動作になります。

グループは自動管理されない ​

SAML 認証では グループ(Groups)の自動管理には対応していません(LDAP 連携ではグループ同期がサポートされています)。管理画面で手動管理する、API でスクリプトから一括管理する、または権限管理の単位を部門にまとめて DeptCode で自動管理する、のいずれかで対応します。

同期はログイン時のみ ​

認証方式同期タイミング同期対象設定
LDAP定期実行(BackgroundService)ディレクトリ内の全ユーザーBackgroundService.json の SyncByLdap: true
SAMLログイン時のみログインした個別ユーザーなし(自動)

GWS 側で部門や名前を変更しても、そのユーザーが次にログインするまでプリザンターには反映されません。

退職者の扱い

GWS 側でユーザーを無効化すれば SAML ログインはできなくなりますが、プリザンター側のユーザーは有効なまま残ります。プリザンター側でもユーザーを無効化する運用(管理画面・API による手動またはスクリプト対応)が必要です。

Secure LDAP の併用 ​

認証は SAML、ユーザー同期は GWS の Secure LDAP(LDAPS)という構成も選べます。

図を読み込み中…

メリットデメリット
SyncByLdap で全ユーザーの属性を定期的に同期できるGWS で Secure LDAP を有効化し、クライアント証明書を発行する必要がある
GWS で無効化したユーザーを LDAP 同期で無効化できるSecure LDAP は Business Plus / Enterprise / Education Fundamentals 以上のエディションのみ
ユーザーがログインしなくても属性変更が反映されるAuthentication.json と BackgroundService.json の両方を管理する必要がある
SSO・多要素認証は SAML 側でそのまま使えるSAML と LDAP で属性マッピングを二重管理する。サーバーから ldap.google.com:636 への通信が必要
json
{
    "SyncByLdap": true,
    "SyncByLdapTime": [ "02:00", "14:00" ]
}
json
{
    "Provider": "SAML",
    "LdapParameters": [
        {
            "LdapSearchRoot": "LDAP://ldap.google.com:636/dc=example,dc=com",
            "LdapSearchProperty": "mail",
            "LdapTenantId": 1,
            "LdapMailAddress": "mail",
            "LdapFirstName": "givenName",
            "LdapLastName": "sn"
        }
    ],
    "SamlParameters": {
        ...
    }
}

WARNING

併用する場合も Provider は "SAML" のままにします。LdapParameters は SyncByLdap によるバックグラウンド同期専用で、認証方式には影響しません。

設定の反映と動作確認 ​

SAML の設定は、プリザンターを再起動(IIS はアプリケーションプールのリサイクル、Linux は sudo systemctl restart pleasanter など)して反映します。

/admins/reloadparameters(パラメータ)では反映されません。確認したソースでは、Provider が SAML かどうかで認証スキームを登録するかを Startup.ConfigureServices で決め、SPOptions と IdentityProviders もそのとき Sustainsys.Saml2 の設定に読み込みます(Startup.cs)。パラメータのリロードは JSON を読み直すだけで、この登録はやり直さないためです。

反映後、ログイン画面に通常のログインフォームに加えて 「SSO ログイン」ボタン が表示されます。ボタンから Google のログイン画面に遷移し、ログインするとトップページに戻ります。初回ログイン時は Attributes のマッピングに従ってユーザーが自動作成されます。

INFO

SAML を有効にしても IdP への自動リダイレクトは行われません。ID / パスワード欄は残り、ローカル認証も引き続き使えます。

SAML ユーザーとローカルユーザーの混在 ​

Provider が "SAML" でも、SSO ログイン(IdP で認証)とローカルログイン(プリザンター DB のパスワードハッシュで認証)は併用できます。どちらでログインしても操作・権限は同じです。段階的な SSO 移行、社内は SSO・外部パートナーはローカル認証、IdP 障害時の管理者ログインといった使い方ができます。

移行完了後にローカルログインを制限したい場合は、テナントの契約設定で AllowOriginalLogin を 0 にします。テナント管理者以外 はローカルログインできなくなります。

INFO

テナント管理者(TenantManager = true)はこの制限の対象外です。IdP 障害時にも管理者がログインして運用を継続できるようにするための設計です。

ローカルログインはログインフォームの流れ(ロックアウト・2 段階認証・パスワード有効期限のチェック)を通りますが、SSO ログインは SAML の応答を受けたところで直接 Cookie を発行するため、プリザンター側の 2 段階認証は求められません。詳しくは 認証方式の内部動作 を見てください。

既存ユーザーを SSO に移行する ​

SAML ログイン時は、IdP から返る NameID を LoginId としてユーザーを照合 します。

図を読み込み中…

したがって Google のメールアドレスと LoginId が一致していれば既存ユーザーがそのまま引き継がれます。

既存の LoginIdGoogle メールアドレス結果
taro@example.comtaro@example.com既存ユーザーを継続利用(作業不要)
tarotaro@example.comそのままだと新規ユーザーが作成される

LoginId がメールアドレスと異なる場合は、SAML 切り替え 前 に LoginId をメールアドレスに変更し、全ユーザーの変更が終わってから Provider を "SAML" にします。LoginId を変えると変更後の ID でしかログインできなくなるため、切り替え直前に一括で変更するのがおすすめです。

ユーザー数が多い場合は SQL で一括更新できます。

DANGER

SQL を直接実行する場合は、必ず事前にバックアップを取ってから行ってください。

sql
-- LoginId をメールアドレスに一括更新する例
UPDATE u
SET u.[LoginId] = ma.[MailAddress]
FROM [Users] u
INNER JOIN [MailAddresses] ma ON u.[UserId] = ma.[OwnerId]
WHERE ma.[OwnerType] = 'Users'
  AND u.[LoginId] <> ma.[MailAddress]
  AND u.[TenantId] = 1;
sql
-- LoginId をメールアドレスに一括更新する例
UPDATE "Users"
SET "LoginId" = ma."MailAddress"
FROM (
    SELECT "OwnerId", "MailAddress"
    FROM "MailAddresses"
    WHERE "OwnerType" = 'Users'
) ma
WHERE "Users"."UserId" = ma."OwnerId"
  AND "Users"."LoginId" <> ma."MailAddress"
  AND "Users"."TenantId" = 1;
sql
-- LoginId をメールアドレスに一括更新する例
UPDATE `Users` u
INNER JOIN `MailAddresses` ma ON u.`UserId` = ma.`OwnerId`
SET u.`LoginId` = ma.`MailAddress`
WHERE ma.`OwnerType` = 'Users'
  AND u.`LoginId` <> ma.`MailAddress`
  AND u.`TenantId` = 1;

推奨手順は次のとおりです。

  1. 事前準備: 全ユーザーの LoginId と Google メールアドレスの対応一覧を作り、DB をバックアップする
  2. LoginId の変更(必要な場合): 変更後、独自認証でログインできることを確認する
  3. SAML の有効化: Authentication.json を編集して反映する
  4. 動作確認: テストユーザーで SAML ログインし、既存データ(レコード・権限設定など)が引き継がれていることを確認する
  5. 全ユーザーへの展開

未登録ユーザーを拒否する ​

json
{
    "Provider": "SAML",
    "RejectUnregisteredUser": true,
    "SamlParameters": {
        ...
    }
}
設定値動作
false(既定)未登録ユーザーは SAML ログイン時に自動作成される
true事前に登録済みのユーザーのみ SAML ログインできる

ログイン画面で SSO ボタンを目立たせる ​

SSO を有効にしたログイン画面は ID / パスワード欄が主役のままなので、拡張スタイルと拡張スクリプトで ID / パスワード欄を折りたたみ、SSO ボタンを上に出せます。IdP の種類に関係なく使えます。

css
/* ログインページのみに適用(body#login で限定) */

/* SSO ログインボタンをフォーム上部に移動して強調 */
body#login #SsoLogin {
  order: -1;
  margin-bottom: 24px;
  padding: 16px;
  background-color: #f0f7ff;
  border: 1px solid #4285f4;
  border-radius: 8px;
}

body#login #SsoLogin .ssoLoginMessage {
  font-size: 1.1em;
  font-weight: bold;
  color: #1a73e8;
}

/* ログインエリアを flexbox で順序制御 */
body#login #Logins {
  display: flex;
  flex-direction: column;
}

/* ID/パスワード欄・ログインボタンを初期状態で折りたたみ */
body#login #login-fields-body,
body#login #LoginCommands {
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-height 0.3s ease, opacity 0.3s ease, margin 0.3s ease;
  margin: 0;
}

/* 展開時のスタイル */
body#login #login-fields-body.expanded,
body#login #LoginCommands.expanded {
  max-height: 300px;
  opacity: 1;
  margin-top: 8px;
}

/* 折りたたみトグルボタン */
body#login #LocalLoginToggle {
  cursor: pointer;
  color: #666;
  font-size: 0.9em;
  text-align: center;
  padding: 8px;
  margin-top: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: #fafafa;
  order: 1;
}

body#login #LocalLoginToggle:hover {
  background: #f0f0f0;
  color: #333;
}

body#login #LocalLoginToggle::before {
  content: "▶ ";
}

body#login #LocalLoginToggle.expanded::before {
  content: "▼ ";
}
js
// ログインページのみで実行
if (document.body.id === 'login') {
  $(function () {
    var $logins = $('#Logins');
    var $fields = $('#login-fields-body');
    var $commands = $('#LoginCommands');

    // 折りたたみトグルボタンを挿入
    var $toggle = $('<div>', {
      id: 'LocalLoginToggle',
      text: 'ID / パスワードでログイン'
    });

    // SSO ボタンの後に配置
    var $sso = $('#SsoLogin');
    if ($sso.length) {
      $sso.after($toggle);
    } else {
      // SSO ボタンがない場合はフォームを折りたたまない
      return;
    }

    // トグルボタンのクリックイベント
    $toggle.on('click', function () {
      var isExpanded = $fields.hasClass('expanded');
      $fields.toggleClass('expanded');
      $commands.toggleClass('expanded');
      $toggle.toggleClass('expanded');
      $toggle.text(isExpanded
        ? 'ID / パスワードでログイン'
        : 'ID / パスワードでログイン');

      // 展開時に LoginId にフォーカス
      if (!isExpanded) {
        setTimeout(function () {
          $('#Users_LoginId').focus();
        }, 300);
      }
    });
  });
}

ファイルを配置してパラメータリロードまたは再起動すると、SSO ボタンが上部に表示され、「ID / パスワードでログイン」をクリックしたときだけ入力欄が展開されます。

ID / パスワード欄を折りたたんだログイン画面

INFO

ファイルベースの拡張スタイル・拡張スクリプトは全ページに読み込まれますが、body#login セレクタと document.body.id === 'login' の条件によりログインページ以外では何もしません。

トラブルシューティング ​

症状考えられる原因対処法
ログイン画面で 500 エラーAuthentication.json の JSON 構文エラーカンマ漏れ・括弧の不一致などを確認
Google ログイン後にエラー画面ACS URL またはエンティティ ID の不一致GWS の ACS の URL を https://<ドメイン>/Saml2/Acs、エンティティ ID を SPOptions.EntityId と同じ値にする
「証明書が見つからない」エラー証明書のインポート先が異なるStoreLocation・StoreName(My)をインポート先と一致させる
「拇印が一致しない」エラー拇印のコピーミススペースやコロンを除いた文字列にする
SAML ログインできるが新規ユーザーになるNameID と既存 LoginId の不一致既存ユーザーの LoginId を Google メールアドレスに変更
未登録ユーザーがログインできないRejectUnregisteredUser が true事前にユーザー登録するか false にする

SMTP 認証に GWS の OAuth を使う 1.5.1.0 以降 ​

1.5.1.0 から、メール送信(SMTP)の認証方式として OAuth 2.0 がサポートされ、アプリパスワードを使わずに送信できるようになりました。GWS ではユーザーの介在なしに送信するため、サービスアカウント と ドメイン全体の委任(Domain-wide Delegation) を組み合わせます。

1. Google Cloud プロジェクトの準備 ​

  1. Google Cloud Console で「新しいプロジェクト」を作成する(名前は Pleasanter-SMTP-Client など)
  2. 「APIとサービス」>「ライブラリ」で「Gmail API」を検索して有効にする

2. 認証情報の作成 ​

  1. OAuth 同意画面: 「APIとサービス」>「OAuth同意画面」で、ユーザータイプ「内部」を選んで必須項目を埋める
  2. サービスアカウント: 「認証情報」>「認証情報を作成」>「サービスアカウント」で作成(例: pleasanter-smtp)し、詳細画面の「キー」タブから JSON の鍵を作成して保存する
  3. OAuth 2.0 クライアント ID: 「認証情報を作成」>「OAuth クライアント ID」で種類「ウェブ アプリケーション」を選び、表示された クライアント ID と クライアントシークレット を控える(プリザンターの設定に使います)

INFO

サービスアカウントの JSON に含まれる client_id と private_key はこの後の設定では使いませんが、大切に保管してください。

3. ドメイン全体の委任 ​

  1. GWS 管理コンソールで「セキュリティ」>「アクセスとデータ管理」>「APIの制御」を開く
  2. 「ドメイン全体の委任」で「新しく追加」する
  3. 次を入力する
    • クライアント ID: サービスアカウントのクライアント ID(OAuth 2.0 クライアント ID ではない点に注意)
    • OAuth スコープ: https://mail.google.com/

4. Mail.json の設定 ​

json
{
  "SmtpHost": "smtp.gmail.com",
  "SmtpPort": 587,
  "SmtpUserName": "sender@example.com",
  "SmtpPassword": "",
  "SmtpEnableSsl": true,
  "SecureSocketOptions": "StartTls",
  "UseOAuth": true,
  "OAuthClientId": "xxxxxxxxxxxx.apps.googleusercontent.com",
  "OAuthClientSecret": "your-client-secret-value",
  "OAuthScope": "https://mail.google.com/",
  "OAuthGrantType": "client_credentials",
  "OAuthTokenEndpoint": "https://oauth2.googleapis.com/token",
  "OAuthDefaultExpiresIn": 3600,
  "OAuthTokenRefreshBufferTime": 300,
  "Encoding": "UTF-8",
  "FixedFrom": "sender@example.com",
  "SupportFrom": "sender@example.com"
}
パラメータ名設定値のポイント
SmtpUserName実際に送信元となるメールアドレス
UseOAuthtrue
OAuthClientId手順 2 で作成した「ウェブ アプリケーション」のクライアント ID
OAuthClientSecret手順 2 で作成したクライアントシークレット
OAuthGrantTypeclient_credentials

確認したソースでは、プリザンターはトークン取得時に OAuthTokenEndpoint へ client_id・client_secret・scope・grant_type の 4 項目を POST するだけで、サービスアカウントの鍵(JSON)は使いません(Smtp.cs)。取得したトークンは SmtpUserName と組にして SMTP の OAuth2 認証に使われます(Smtp.cs)。この要求を Google のトークンエンドポイントが受け付けるかは Google 側の仕様で、ソースからは確認できません。

設定後、プリザンターを再起動(IIS の再起動など)してテストメールを送信します。

OAuth 送信の仕組み ​

UseOAuth が true のとき、SMTP サーバーに接続したあと、アクセストークンを取得して MailKit の SaslMechanismOAuth2(XOAUTH2)で認証します。このとき SmtpUserName と SmtpPassword による通常の認証は行いません(Smtp.cs#L199-L213)。

図を読み込み中…

パラメータ既定値内容
UseOAuthfalseOAuth 2.0 で認証する
OAuthClientIdnullクライアント ID
OAuthClientSecretnullクライアントシークレット
OAuthScopenullトークン取得時の scope
OAuthGrantTypenullトークン取得時の grant_type(通常 client_credentials)
OAuthTokenEndpointnullトークンエンドポイントの URL
OAuthDefaultExpiresIn3600応答に expires_in が無いときの有効期間(秒)
OAuthTokenRefreshBufferTime300期限のこの秒数前になったら取り直す
  • トークンのキャッシュ: 取得したトークンは Sessions テーブルに、セッション GUID SmtpOAuthToken・キー OAuthToken:{OAuthClientId} で保存されます(Smtp.cs#L222-L251)。DB に保存されるので、複数台のサーバーで同じトークンを使い回せます。
  • パラメータの検証: トークンを取りに行く前に OAuthClientId・OAuthClientSecret・OAuthScope・OAuthGrantType・OAuthTokenEndpoint が空でないか、OAuthTokenEndpoint が http / https の絶対 URL かを確かめ、足りなければ OAuth parameters are not configured: … の例外になります(Smtp.cs#L323-L359)。
  • 送る内容は 4 項目だけ: POST するのは client_id・client_secret・scope・grant_type のフォームだけです。OAuthGrantType は自由に書けますが、JWT のアサーションなど他の項目は送れないため、実質的にクライアント資格情報(client credentials)方式だけに対応します。
  • エラーの記録: 送信処理の例外は画面には出ず、SysLogs に記録されます。トークン取得の失敗は OAuth token acquisition failed. Status: …、応答の JSON が読めないときは OAuth response JSON parsing failed、access_token が無いときは OAuth response validation failed(例外メッセージ access_token not found in OAuth response)が付きます。
  • シークレットを環境変数で渡す: OAuthClientId と OAuthClientSecret は、Mail.json の値が空なら環境変数 {EnvironmentName}_Mail_OAuthClientId → {ServiceName}_Mail_OAuthClientId(シークレットも同様に _Mail_OAuthClientSecret)の順に読まれます(Initializer.cs#L224-L231)。Mail.json にシークレットを書かずに済みます。

Microsoft 365(Exchange Online)の場合 ​

Entra ID にアプリを登録し、クライアント資格情報でトークンを取ります。

  1. Entra ID の「アプリの登録」で新規登録する(名前は Pleasanter-SMTP など)
  2. 「API のアクセス許可」でアプリケーションの許可を追加し、管理者の同意を与える
  3. 「証明書とシークレット」でクライアントシークレットを作り、値を控える
  4. 概要ページの アプリケーション(クライアント)ID と ディレクトリ(テナント)ID を控える
  5. Exchange 管理センターで、送信に使うメールボックスの SMTP 認証を有効にする
json
{
  "SmtpHost": "smtp.office365.com",
  "SmtpPort": 587,
  "SmtpUserName": "noreply@example.com",
  "SmtpEnableSsl": true,
  "SecureSocketOptions": "StartTls",
  "UseOAuth": true,
  "OAuthClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "OAuthClientSecret": "your-client-secret",
  "OAuthScope": "https://outlook.office365.com/.default",
  "OAuthGrantType": "client_credentials",
  "OAuthTokenEndpoint": "https://login.microsoftonline.com/<テナント ID>/oauth2/v2.0/token",
  "OAuthDefaultExpiresIn": 3600,
  "OAuthTokenRefreshBufferTime": 300,
  "FixedFrom": "\"Pleasanter\" <noreply@example.com>"
}
  • OAuthScope は https://outlook.office365.com/.default です(https://graph.microsoft.com/.default ではありません)。
  • SmtpUserName には実際に送信するメールボックスのアドレスを入れます。送信元にできるメールボックスを絞るには Exchange Online 側のアクセスポリシーを使います。
  • 必要なアクセス許可の種類やメールボックス側の登録など、Entra ID・Exchange Online 側の設定の詳細は Microsoft のドキュメントに従ってください(ここに書いた手順以上のことはプリザンターのソースからは確認できません)。

OAuth を使わない方法(GWS の SMTP リレー) ​

GWS では、管理コンソールで SMTP リレーサービスを設定してプリザンターのサーバーの IP アドレスを許可すれば、IP アドレスで認証して送れます。この場合は UseOAuth を false にし、SmtpUserName / SmtpPassword を空にします(両方が入っているときだけ通常の認証を行います)。

json
{
  "SmtpHost": "smtp-relay.gmail.com",
  "SmtpPort": 587,
  "SmtpEnableSsl": true,
  "SecureSocketOptions": "StartTls",
  "UseOAuth": false,
  "FixedFrom": "\"Pleasanter\" <noreply@example.com>"
}

よくあるエラー ​

エラー確認すること
OAuth token acquisition failedクライアント ID / シークレットが正しいか。OAuth スコープに https://mail.google.com/ が正しく入っているか
Authentication failedSmtpUserName がドメイン全体の委任で許可したドメイン内のアドレスか。委任の設定は反映まで数分かかる場合がある。M365 では SMTP 認証の有効化とアクセス許可の同意を確認
OAuth parameters are not configuredOAuthClientId・OAuthClientSecret・OAuthScope・OAuthGrantType・OAuthTokenEndpoint がすべて入っているか。エンドポイントが https:// から始まる URL か
access_token not found in OAuth responseトークンエンドポイントの URL、クライアント資格情報が正しいか

関連ページ ​

変更履歴

第10版記事の確認版を繰り返す表現を整理する
第9版SSO ユーザーの設定変更 SQL を3種類のDBMSに対応
第8版「認証(Google Workspace の SAML SSO / SMTP の OAuth)」の画像をこのサイトで配信するようにする
第7版拡張ライブラリの読み込みと開発・デバッグ、拡張ヘッドリンク、SMTP の OAuth 送信の解説と、多言語・外部公開カレンダー・スレッド型サイトなどの改修・設計メモを追加
第6版認証方式(2 段階認証・パスキー・LDAP・フォールバック)と認証基盤の解説、関連する改修・設計メモを追加
第5版「外部連携・AI」「構築・運用」「内部実装を読む」に対応バージョンを表示
第4版「構築・運用」を 1.5.8.1 のソースで検証して修正
第3版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第2版「構築・運用」に Microsoft Entra ID での SAML SSO を追加
第1版「構築・運用」セクションの記事を追加