ADの「次回ログオン時にパスワード変更が必要」の実装とパスワードの有効期限に関する考察

1. はじめに

この問題、発端は IISに搭載されている.Net ASP アプリの iisadmpwd が、パスワード期限切れユーザや、初回ログオン前のユーザを正しく処理できなくて、MSがKBに「ADSIの仕様だから」と放言していたことでした。

ところが、MSDNに「パスワードの有効期限はいつまでですか?」という記事があり、少なくとも期限切れユーザはハンドルできるという記述があります。これはこれで不正確な記述もあって困り物ですが、有用な資料であることは間違いありません。(少なくとも私がこの記事をまとめようという意欲をかきたてる程度には。)

GUI(管理パック)やスクリプトで処理した内容をADSIでどう参照できるのか、その動作について追ってみたいと思います。

2. ADパスワードの有効期限

Active Directory のユーザオブジェクトには一般に、パスワードの期限があります。もちろん、アカウントオプションで、パスワードを無期限にしたり、ユーザがパスワードを変更できなくしたり、ドメインポリシーで設定を加えていれば別ですが。

ただし、そういった小細工を加えていないユーザであっても、GUIからパスワードの期限がいつ切れるのかを参照する方法が用意されていません。

さらに、前述のMSDNの資料を読めば分かりますが、フィールド1発抜き出すだけで分かるものでもありません。有効期限を求める処理フローの話はおいとくとして、ADSI の LDAP や SQL のクエリーでさくっと検索できるものではないということだけ、まずご理解ください。

なお、GUIの画面にはアカウントの有効期限というオプションがありますが、もちろんこれはパスワードの有効期限とは無関係です。

3. パスワード変更の強制

ADのIDを新規作成した際や、エンドユーザがパスワードを忘れた際にパスワードを初期化した場合、「次回ログオン時にパスワード変更が必要」というオプションを有効にすることがよくあると思います。エンドユーザにとっては同じ動きになるのですが、実はこの処理、私が知るだけで3つの方法が存在します。

(1) GUI で管理者による強制パスワード変更時、あるいはアカウントオプションで、
  「次回ログオン時にパスワード変更が必要」オプションを有効にする

(2) ADSI でたとえば以下のような処理を行う。
    '******************************************************************
    '* 次回ログイン時にパスワード変更をするよう設定
    '******************************************************************
    Set adsUser = GetObject("WinNT://" & DOMAIN_NAME & "/" & strUserID & ",user")
     '省略
    adsUser.Put "PasswordExpired", CLng(1)
    adsUser.SetInfo
(3) ADSIでたとえば以下のような処理を行う。
    set objADUser = GetObject("LDAP://" & strUserDN )
    IF Err Then
     '省略
    Else
     objADUser.Put "pwdLastSet", 0
     objADUser.SetInfo
よく見れば、(2) はパスワードを有効期限切れに設定している処理であり、(3)はパスワードの最終変更日を 0 (≒ 1601年1月1日午前0時)に設定しています。

(3)の方法が「次回ログオン時にパスワード変更が必要」という挙動を呼び起こすコーディングであることは、MS の Script Center のかの有名な「Hey Scripting Guy」でも触れられている、よく知られた方法です。

(2)の方法についてはいまどきの資料になかなか見当たらず、MSDNのこの資料くらい。

ただし、「ADSIスクリプトによるWindows2000システム管理」(ピアソン・エデュケーション刊)など、解説図書にはよく出ているようです。どうも本来はローカルユーザの管理用という位置づけで、WindowsRPCを利用してリモート実行もできますよ、くらいの話のようです。

なお冒頭で触れた「パスワードの有効期限はいつまでですか?」の記事にもあるように、Active Directory のドメインユーザに対して何か操作をするなら、WinNTプロバイダではなく今後は LDAP プロバイダを使いなさいというのが公式見解です。

4. 実験

では、どのような方法でセットしても同じような結果が得られるのか確認してみたいと思います。サンプルとして以下のようなユーザを用意しました。

fig01

そして、これをいじるスクリプト2種類、状態表示のスクリプトを2種類用意します。ADドメイン名は example.local とします。

set_winnt.vbs
'On Error Resume Next
const DOMAINNAME = "example"

Set objUser = GetObject("WinNT://"& DOMAINNAME &"/foobar,user")
objUser.Put "PasswordExpired", CLng(1)
objUser.Setinfo

set_ldap.vbs
'On Error Resume Next
strDomain="DC=example,DC=local"
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D     

Set objUser = GetObject("LDAP://" & ADlookup("foobar"))
objUser.Put "pwdLastSet", 0
objUser.SetInfo

Wscript.quit

Function ADlookup(ID)
	Dim dtStart,objConnection,objCommand,objRecordSet

	dtStart = TimeValue(Now())
	Set objConnection = CreateObject("ADODB.Connection")
	objConnection.Open "Provider=ADsDSOObject;"

	Set objCommand = CreateObject("ADODB.Command")
	objCommand.ActiveConnection = objConnection

	objCommand.CommandText = ";(&(objectCategory=user)" & "(samAccountName=" & ID & "));distinguishedName;subtree"

	Set objRecordSet = objCommand.Execute
 
	IF objRecordset.RecordCount = 0 Then
		ADlookup = ""
	else
		ADlookup = objRecordset.Fields("distinguishedName")
	End If
	objConnection.Close
	set objCommand = nothing
End Function

show_winnt.vbs
'On Error Resume Next
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D
const DOMAINNAME = "example"

Set objUser = GetObject("WinNT://"& DOMAINNAME &"/foobar,user")

If objUser.Get("passwordexpired") <> 0 then
	Wscript.echo "パスワードの期限が切れています"
Else
	Wscript.Echo "パスワードは有効期限内です"
End if
show_ldap.vbs
On Error Resume Next
strDomain="DC=example,DC=local"
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D

Set objUser = GetObject("LDAP://" & ADlookup("foobar"))
dtmValue = objUser.PasswordLastChanged

If Err.Number = E_ADS_PROPERTY_NOT_FOUND Then
    WScript.Echo "パスワードが設定されていません。"
    WScript.Quit
Else  
    WScript.Echo "パスワードは " & _
                 DateValue(dtmValue) & " の " & TimeValue(dtmValue) & _
                 " に最後に設定されました。"
End If

Wscript.quit

Function ADlookup(ID)
'(さっきと同じ)
End Function

を用意します show_winnt は set_winnt でセットしたフィールドを読み出して判定。show_ldap は MSDN のサンプルをいじったスクリプトです。

[パターン1] アカウントは普通に使用できる状態

D:\>cscript //nologo show_ldap.vbs
パスワードは 2011/06/29 の 11:36:46 に最後に設定されました。
                                                                  
D:\>cscript //nologo show_winnt.vbs
パスワードは有効期限内です

[パターン2] パスワードを手動で初期化した状態

D:\>cscript //nologo show_ldap.vbs
パスワードが設定されていません。

※エラートラップをしないとこうなります↓
D:\>cscript //nologo show_ldap.vbs
D:\show_ldap.vbs(7, 1) (null): 0x8000500D
                                                                  
D:\>cscript //nologo show_winnt.vbs
パスワードの期限が切れています

[パターン3] この状態でパスワード変更強制オプションだけを手動解除

D:\>cscript //nologo show_ldap.vbs
パスワードは 2011/06/29 の 11:36:46 に最後に設定されました。
                                                                  
D:\>cscript //nologo show_winnt.vbs
パスワードは有効期限内です

これで、show_ldap.vbs つまり、MSDNの記事にある説明は不適切で、パスワードがセットされていないわけではなく、以前に設定したパスワードも記憶されていることがすぐに確認できます。
0x8000500Dというエラーコードは E_ADS_PROPERTY_NOT_FOUND というシンボルが割り当てられています。言葉どおりに取れば、たとえばユーザIDを間違えて存在しないものを指定してもこのエラーになりそうに思いますが、これは実験すると 0x80005000 ( E_ADS_BAD_PATHNAME )というエラーになりますので、少なくともこの処理においては 0x8000500D エラーが出れば「次回ログオン時に…」がセットされていると考えてよい(そうせざるを得ない)ようです。

[パターン4] WinNTプロバイダでパスワード期限切れにする

D:\>cscript //nologo show_ldap.vbs
パスワードが設定されていません。
                                                                  
D:\>cscript //nologo show_winnt.vbs
パスワードの期限が切れています

GUI では「次回…」にチェックが入った状態でした。

では手動で「次回…」のフラグを外します。

[パターン5] LDAPプロバイダでパスワード期限切れにする

D:\>cscript //nologo show_ldap.vbs
パスワードが設定されていません。
                                                                  
D:\>cscript //nologo show_winnt.vbs
パスワードの期限が切れています

GUI では「次回…」にチェックが入った状態でした。

これで、だいたい想定している範囲内では動作が等価であるということはいえそうです。

5. 結論

※ADのパスワードがいつ切れるかを調べる方法については、上記内容にドメインポリシーを加味すれば計算できることが分かると思います。それが冒頭に紹介したMSDNの記事になります。

戻る