Тема: VB.NET: Авторизация OpenVPN через учётку Windows
Есть в природе OpenVPN, умеющий при авторизации пользователей запускать скрипт для дополнительной авторизации. Под линуксы скриптов навалом, под Windows нашел одну портянку на vbs, которая к тому же не заработала на моей ОС(так как AD там нет и быть не может).
Как оказалось, всё гораздо проще можно реализовать воспользовавшись .NET. Под данную реализацию потребуется версия 3.5(и выше - требуется наличие System.DirectoryServices.AccountManagement.PrincipalContext).
Для тех кто не сталкивался с OpenVPN в кратце поясню. При авторизации пользователя(помимо сертификатов и пр.) можно запросить у пользователя логин-пароль и проверить его в системных учётках ОС. Для этого в конфиге указывается скрипт, которому при запуске в виде параметра передаётся имя файла. В файле(временный, на время авторизации) содержится всего две строки - логин и пароль. В результате работы скрипта должен вернуться код завершения 0(успешная авторизация) или 1(не удалось авторизоваться).
Протестировано на:
Windows XP Pro SP3, VS2008(.NET 3.5)
Windows 2003 Server Web Edition, VB.NET 2010 Express(.Net 4.0 - на сайте MS 2008-ую эспресс студию уже не нашел)
Приложение консольное. В проекте нужно добавить ссылку на System.DirectoryServices.AccountManagement.
Программа умеет:
1. Проверять пару логин-пароль.
2. Проверять нахождение пользователя в группе(непосредственно, без проверки вхождения через членство в других группах).
3. Работает для локальной авторизации и для домена(про домен - вроде работает, но мой комп не в домене и тест нельзя назвать полноценным).
4. Пишет лог(в файл).
5. Настройки берёт из INI-файла.
Пути сборок для наглядности не сокращал.
Основной модуль:
Module winauth
Dim logfile As System.IO.StreamWriter
Dim pwdfile As System.IO.StreamReader
Dim INI As New Class_ini_ops
Function Main(ByVal cmdArgs() As String) As Integer
Dim oContext As System.DirectoryServices.AccountManagement.PrincipalContext
Dim oGroupUsers As System.DirectoryServices.AccountManagement.GroupPrincipal
Dim usrName As String
Dim usrPass As String
Dim returnValue As Boolean
Dim ini_file = System.Environment.CurrentDirectory.ToString & "\" & "config.ini"
Dim ini_section = "Configuration"
'chk ini file
If INI.GetSectionParams(ini_file, ini_section) Is Nothing Then
Echo("Incorrect INI format.")
Return 1
End If
Dim GroupName As String
GroupName = INI.GetParamValue(ini_file, ini_section, "GroupName")
If GroupName Is Nothing Then
Echo("User target group not set in ini-file.")
Return 1
End If
'имя(и путь) файла вынести в конфиг
logfile = My.Computer.FileSystem.OpenTextFileWriter("winauth.log", True, _
System.Text.Encoding.GetEncoding(1251))
logfile.AutoFlush = True
'check args
If cmdArgs.Count <> 1 Then
Echo("Wrong parametrs.")
Return 1
Else
Try
pwdfile = My.Computer.FileSystem.OpenTextFileReader(cmdArgs(0).ToString, _
System.Text.Encoding.GetEncoding(1251))
Catch ex As Exception
Echo(ex.Message.ToString)
Echo("pwdfile not found.")
End Try
Try
usrName = pwdfile.ReadLine.ToString
Catch ex As Exception
Echo(ex.Message.ToString)
Echo("No data in pwdfile.")
Return 1
End Try
Try
usrPass = pwdfile.ReadLine.ToString
Catch ex As Exception
Echo(ex.Message.ToString)
Echo("No password in pwdfile.")
Return 1
End Try
End If
pwdfile.Close()
'Get Auth context from INI, default is Local
Dim iniContext
iniContext = INI.GetParamValue(ini_file, ini_section, "Context")
If iniContext Is Nothing Then
iniContext = "Local"
End If
' Set PrincipalContext
Dim DN As String = ""
Dim PDC As String = ""
Select Case iniContext
Case "Local"
oContext = New System.DirectoryServices.AccountManagement.PrincipalContext( _
System.DirectoryServices.AccountManagement.ContextType.Machine, _
System.Environment.MachineName.ToString)
Case "Domain"
PDC = INI.GetParamValue(ini_file, ini_section, "PDC")
If PDC Is Nothing Then
Echo("PDC not set in ini-file.")
Return 1
End If
DN = INI.GetParamValue(ini_file, ini_section, "DN")
If DN Is Nothing Then
Echo("DN not set in ini-file.")
Return 1
End If
oContext = New System.DirectoryServices.AccountManagement.PrincipalContext( _
System.DirectoryServices.AccountManagement.ContextType.Domain, _
PDC, DN)
Case Else
Echo("Context not set in ini-file or incorrect.")
Return 1
End Select
'Check that the user is a member of the group
oGroupUsers = System.DirectoryServices.AccountManagement.GroupPrincipal.FindByIdentity(oContext, _
DirectoryServices.AccountManagement.IdentityType.Name, _
GroupName)
Dim oUsers As System.DirectoryServices.AccountManagement.PrincipalSearchResult(Of _
System.DirectoryServices.AccountManagement.Principal)
oUsers = Nothing
If oGroupUsers Is Nothing Then
Echo("No such group [" & GroupName & "] found.")
Return 1
Else
oUsers = oGroupUsers.GetMembers
End If
If oUsers.Contains(System.DirectoryServices.AccountManagement.Principal.FindByIdentity(oContext, usrName)) Then
Debug.Print("caught!")
Else
Debug.Print("shot at milk...")
Select Case iniContext
Case "Local"
Echo("Group check failure: the user is not a member of [" & GroupName & "].")
Case "Domain"
Echo("Group check failure: the user is not a member of [" & GroupName & "@{" & DN & "}].")
End Select
Return 1
End If
Try
returnValue = oContext.ValidateCredentials(usrName, usrPass)
Catch ex As Exception
Echo(ex.Message.ToString)
Return 1
'Error Codes list for Microsoft technologies:
'http://www.symantec.com/business/support/index?page=content&id=TECH12638
'"HRESULT: 0x80070533" == "Logon failure: account currently disabled."
End Try
Select Case returnValue
Case False
Echo("Logon failure: username or password incorrect.")
Return 1
Case True
Echo("Logon is successful.")
Return 0
Case Else
Echo("Unknown Error?")
Return 1
End Select
End Function
Sub Echo(ByVal text)
Console.WriteLine(text)
Debug.Print(text)
logfile.WriteLine(Now() & ":> " & text)
End Sub
End Module
Клас для работы с INI:
Public Class Class_ini_ops
'изначально была копипаста отсюда: http://www.cyberforum.ru/vb-net/thread382768.html
#Region "API Calls"
Private Declare Unicode Function WritePrivateProfileString Lib "kernel32" _
Alias "WritePrivateProfileStringW" (ByVal lpApplicationName As String, _
ByVal lpKeyName As String, ByVal lpString As String, _
ByVal lpFileName As String) As Integer
Private Declare Unicode Function GetPrivateProfileString Lib "kernel32" _
Alias "GetPrivateProfileStringW" (ByVal lpApplicationName As String, _
ByVal lpKeyName As String, ByVal lpDefault As String, _
ByVal lpReturnedString As String, ByVal nSize As Int32, _
ByVal lpFileName As String) As Integer
#End Region
ReadOnly CallBufferSize As Short = 4096
Public Function GetSectionsList(ByVal INIfile As String) As Array
Dim n As Integer
Dim sData As String
Dim sb As New System.Text.StringBuilder
sData = sb.Insert(0, vbNullChar, CallBufferSize).ToString
n = GetPrivateProfileString(vbNullString, vbNullString, vbNullString, sData, CallBufferSize, INIfile)
If n > 0 Then
GetSectionsList = sData.Substring(0, n - 1).Split(vbNullChar)
Else
GetSectionsList = Nothing
End If
End Function
Public Function GetSectionParams(ByVal INIfile As String, ByVal SectionName As String) As Array
Dim n As Integer
Dim sData As String
Dim sb As New System.Text.StringBuilder
sData = sb.Insert(0, vbNullChar, CallBufferSize).ToString
n = GetPrivateProfileString(SectionName, vbNullString, vbNullString, sData, CallBufferSize, INIfile)
If n > 0 Then
GetSectionParams = sData.Substring(0, n - 1).Split(vbNullChar)
Else
GetSectionParams = Nothing
End If
End Function
Public Overloads Function GetParamValue(ByVal INIfile As String, ByVal SectionName As String, ByVal ParamName As String) As String
Dim n As Integer
Dim sData As String
Dim sb As New System.Text.StringBuilder
sData = sb.Insert(0, vbNullChar, CallBufferSize).ToString
n = GetPrivateProfileString(SectionName, ParamName, vbNullString, sData, CallBufferSize, INIfile)
If n > 0 Then
GetParamValue = sData.Substring(0, n)
Else
GetParamValue = Nothing
End If
End Function
Public Overloads Function GetParamValue(ByVal INIfile As String, ByVal SectionName As String, ByVal ParamName As String, _
ByVal DefaultValue As String) As String
Dim n As Integer
Dim sData As String
Dim sb As New System.Text.StringBuilder
sData = sb.Insert(0, vbNullChar, CallBufferSize).ToString
n = GetPrivateProfileString(SectionName, ParamName, DefaultValue, sData, CallBufferSize, INIfile)
If n > 0 Then
GetParamValue = sData.Substring(0, n)
Else
GetParamValue = Nothing
End If
End Function
Public Function SetParamValue(ByVal INIfile As String, ByVal SectionName As String, ByVal ParamName As String, _
ByVal ParamValue As String) As Boolean
Call WritePrivateProfileString(SectionName, ParamName, ParamValue, INIfile)
If ParamValue = GetParamValue(INIfile, SectionName, ParamName) Then
SetParamValue = True
Else
SetParamValue = False
End If
End Function
End Class
Пример конфига(config.ini, ищется в папке в приложением):
[Configuration]
;set auth context
;Local - check local credentials(default)
;Domain - check credentials in Domain
Context = Local
; Used only in Context = Domain
PDC = MyServer
DN = "DC=mydomain,DC=local"
;target user group
GroupName = "Операторы сервера"
P.S. Решение "вылежалось" с пол года, так что публикую как есть. Будут коментарии -> тема в обсуждениях: Серый форум ? Общение ? Прочие скриптовые технологии и близкие к ним ? VB.NET: Авторизация OpenVPN через учётку Windows