Проверить имя пользователя и пароль в Active Directory?

527

Как я могу проверить имя пользователя и пароль в Active Directory? Я просто хочу проверить правильность имени пользователя и пароля.

Марк
источник

Ответы:

643

Если вы работаете в .NET 3.5 или новее, вы можете использовать System.DirectoryServices.AccountManagementпространство имен и легко проверить свои учетные данные:

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAIN"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "mypassword");
}

Это просто, это надежно, это 100% C # управляемый код с вашей стороны - что еще вы можете попросить? :-)

Прочтите все об этом здесь:

Обновить:

Как указано в этом другом вопросе SO (и его ответах) , существует проблема с этим вызовом, который может возвращаться Trueдля старых паролей пользователя. Просто помните об этом и не удивляйтесь, если это произойдет :-) (спасибо @MikeGledhill за это!)

marc_s
источник
36
В моем домене я должен был указать pc.ValidateCredentials («myuser», «mypassword», ContextOptions.Negotiate), иначе я бы получил System.DirectoryServices.Protocols.DirectoryOperationException: сервер не может обрабатывать запросы каталогов.
Алекс Пек
12
Если срок действия пароля истек или учетные записи отключены, то ValidateCredentials вернет false. К сожалению, он не говорит вам, почему возвращается false (что очень жаль, поскольку я не могу сделать что-то разумное, например, перенаправить пользователя на изменение его пароля).
Крис Джей
65
Также остерегайтесь учетной записи «Гость» - если учетная запись гостя на уровне домена включена, ValidateCredentials возвращает значение true, если вы предоставляете ей несуществующего пользователя. В результате вы можете позвонить, UserPrinciple.FindByIdentityчтобы узнать, существует ли переданный идентификатор пользователя первым.
Крис Джей
7
@AlexPeck: причина, по которой вам пришлось это делать (как и мне), заключалась в том, что .NET по умолчанию использует следующие технологии: LDAP + SSL, Kerberos, затем RPC. Я подозреваю, что RPC отключен в вашей сети (хорошо!), И Kerberos фактически не используется .NET, если вы явно не скажете это с помощью ContextOptions.Negotiate.
Бретт Веенстра
5
Имейте в виду, что если пользователь ИЗМЕНИТ свой пароль Active Directory, этот фрагмент кода продолжит успешно аутентифицировать пользователя, используя свой старый пароль AD. Да, действительно. Прочитайте здесь: stackoverflow.com/questions/8949501/…
Майк Гледхилл
70

Мы делаем это в нашей интранете

Вы должны использовать System.DirectoryServices;

Вот кишки кода

using (DirectoryEntry adsEntry = new DirectoryEntry(path, strAccountId, strPassword))
{
    using (DirectorySearcher adsSearcher = new DirectorySearcher(adsEntry))
    {
        //adsSearcher.Filter = "(&(objectClass=user)(objectCategory=person))";
        adsSearcher.Filter = "(sAMAccountName=" + strAccountId + ")";

        try
        {
            SearchResult adsSearchResult = adsSearcher.FindOne();
            bSucceeded = true;

            strAuthenticatedBy = "Active Directory";
            strError = "User has been authenticated by Active Directory.";
        }
        catch (Exception ex)
        {
            // Failed to authenticate. Most likely it is caused by unknown user
            // id or bad strPassword.
            strError = ex.Message;
        }
        finally
        {
            adsEntry.Close();
        }
    }
}
DiningPhilanderer
источник
9
Что вы положили в «путь»? Название домена? Название сервера? Путь LDAP к домену? Путь LDAP к серверу?
Ян Бойд
3
Ответ 1: Нет, мы запускаем его как веб-сервис, поэтому его можно вызывать из нескольких мест в основном веб-приложении. Ответ 2: Путь содержит информацию LDAP ... LDAP: // DC = имя домена1, DC = имя домена2, DC = com
DiningPhilanderer
3
Казалось бы, это может позволить внедрение LDAP. Возможно, вы захотите убедиться, что вы избежали или удалили любые скобки в strAccountId
Brain2000
Означает ли это, что strPasswordхранится в LDAP в виде простого текста?
Мэтт Коджэй
15
Там никогда не должно быть необходимости явного вызова Close()на usingпеременном.
Нергудс
62

В некоторых решениях, представленных здесь, отсутствует возможность различать неправильный пользователь / пароль и пароль, который необходимо изменить. Это можно сделать следующим образом:

using System;
using System.DirectoryServices.Protocols;
using System.Net;

namespace ProtocolTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                LdapConnection connection = new LdapConnection("ldap.fabrikam.com");
                NetworkCredential credential = new NetworkCredential("user", "password");
                connection.Credential = credential;
                connection.Bind();
                Console.WriteLine("logged in");
            }
            catch (LdapException lexc)
            {
                String error = lexc.ServerErrorMessage;
                Console.WriteLine(lexc);
            }
            catch (Exception exc)
            {
                Console.WriteLine(exc);
            }
        }
    }
}

Если пароль пользователя неверен или пользователь не существует, ошибка будет содержать

«8009030C: LdapErr: DSID-0C0904DC, комментарий: ошибка AcceptSecurityContext, данные 52e, v1db1»,

если необходимо изменить пароль пользователя, он будет содержать

«8009030C: LdapErr: DSID-0C0904DC, комментарий: ошибка AcceptSecurityContext, данные 773, v1db1»

Значение lexc.ServerErrorMessageданных представляет собой шестнадцатеричное представление кода ошибки Win32. Это те же самые коды ошибок, которые были бы возвращены, в противном случае вызывая вызов Win32 LogonUser API. Список ниже суммирует диапазон общих значений с шестнадцатеричными и десятичными значениями:

525 user not found ​(1317)
52e invalid credentials ​(1326)
530 not permitted to logon at this time (1328)
531 not permitted to logon at this workstation (1329)
532 password expired ​(1330)
533 account disabled ​(1331) 
701 account expired ​(1793)
773 user must reset password (1907)
775 user account locked (1909)
Сорен Морс
источник
2
К сожалению, некоторые установки AD не возвращают код ошибки LDAP, что означает, что это решение не будет работать.
Сорен Морс
4
Не забудьте добавить некоторые ссылки на проект: System.DirectoryServicesиSystem.DirectoryServices.Protocols
TomXP411
3
Мой вопрос, однако, заключается в следующем: как вы получаете имя сервера LDAP? Если вы пишете переносимое приложение, вы не можете ожидать, что пользователь будет знать или должен будет указывать имена серверов AD в каждой сети.
TomXP411
1
У меня есть пользователи, которым разрешен вход на определенные рабочие станции; Как мне указать рабочую станцию, для которой я пытаюсь войти? (рабочая
станция1
1
Я чувствую себя странно, так как не думаю, что вы получаете достаточно кредитов. Это полностью управляемый метод, без проблем с вызовом Win32 API, чтобы определить, должен ли «пользователь сбросить пароль», чего явно не было ни в одном другом ответе. Есть ли в этом методе лазейка, вызывающая низкую оценку? хм ...
Лионет Чен
34

очень простое решение с использованием DirectoryServices:

using System.DirectoryServices;

//srvr = ldap server, e.g. LDAP://domain.com
//usr = user name
//pwd = user password
public bool IsAuthenticated(string srvr, string usr, string pwd)
{
    bool authenticated = false;

    try
    {
        DirectoryEntry entry = new DirectoryEntry(srvr, usr, pwd);
        object nativeObject = entry.NativeObject;
        authenticated = true;
    }
    catch (DirectoryServicesCOMException cex)
    {
        //not authenticated; reason why is in cex
    }
    catch (Exception ex)
    {
        //not authenticated due to some other exception [this is optional]
    }

    return authenticated;
}

доступ к NativeObject необходим для обнаружения неверного пользователя / пароля

Стивен А. Лоу
источник
4
Этот код плох, потому что он также выполняет проверку авторизации (проверьте, разрешено ли пользователю читать информацию активного каталога). Имя пользователя и пароль могут быть действительными, но пользователю не разрешено читать информацию - и получить исключение. Другими словами, вы можете иметь действительное имя пользователя и пароль, но все равно получите исключение.
Ян Бойд
2
я на самом деле в процессе запроса нативный эквивалент PrincipleContext- который существует только в .NET 3.5. Но если вы используете .NET 3.5 или новее, вы должны использоватьPrincipleContext
Ian Boyd
28

К сожалению, не существует «простого» способа проверить учетные данные пользователей в AD.

С каждым представленным методом вы можете получить ложноотрицательный результат: кредиты пользователя будут действительны, однако AD вернет ложь при определенных обстоятельствах:

  • Пользователь должен сменить пароль при следующем входе в систему.
  • Срок действия пароля пользователя истек.

ActiveDirectory не позволит вам использовать LDAP для определения того, является ли пароль недействительным из-за того, что пользователь должен сменить пароль или срок его действия истек.

Чтобы определить изменение пароля или срок его действия истек, вы можете вызвать Win32: LogonUser () и проверить код ошибки Windows на наличие следующих 2 констант:

  • ERROR_PASSWORD_MUST_CHANGE = 1907
  • ERROR_PASSWORD_EXPIRED = 1330
Алан
источник
1
Могу ли я спросить, где вы получили предсказания для Expired и Must_Change ... Нашли их нигде, кроме здесь :)
mabstrei
1
Из статьи MSDN: msdn.microsoft.com/en-us/library/windows/desktop/…
Алан
Спасибо. Я пытался выяснить, каким образом моя проверка все время возвращала ложь. Это было потому, что пользователь должен изменить свой пароль.
Deise Vicentin
22

Вероятно, самый простой способ это PInvoke LogonUser Win32 API.eg

http://www.pinvoke.net/default.aspx/advapi32/LogonUser.html

Ссылка MSDN здесь ...

http://msdn.microsoft.com/en-us/library/aa378184.aspx

Определенно хотите использовать тип входа

LOGON32_LOGON_NETWORK (3)

Это создает только легкий токен - идеально подходит для проверок AuthN. (другие типы могут использоваться для создания интерактивных сессий и т. д.)

stephbu
источник
Как указывает @Alan, у LogonUser API есть много полезных особенностей, помимо вызова System.DirectoryServices.
Stephbu
3
@cciotti: Нет, это не так. Лучший способ правильно аутентифицировать кого-либо - это использовать LogonUserAPI для записи @stephbu. Все остальные методы, описанные в этом посте, НЕ будут работать на 100%. Тем не менее, я просто считаю, что вы должны быть присоединены к домену, чтобы позвонить в LogonUser.
Алан
@ Алан, чтобы сгенерировать учетные данные, вы должны иметь возможность подключиться к домену, передав действующую учетную запись домена. Однако я уверен, что ваша машина не обязательно должна быть членом домена.
Стефбу
2
LogonUserAPI требует от пользователя иметь Действовать как часть операционной системы privelage; это не то, что получают пользователи - и не то, что вы хотите предоставить каждому пользователю в организации. ( msdn.microsoft.com/en-us/library/aa378184(v=vs.85).aspx )
Ян Бойд,
1
LogonUser нуждается только в действии как часть операционной системы для Windows 2000 и ниже согласно support.microsoft.com/kb/180548 ... Это выглядит чисто для Server 2003 и выше.
Крис Джей
18

Полное решение .Net заключается в использовании классов из пространства имен System.DirectoryServices. Они позволяют запрашивать сервер AD напрямую. Вот небольшой пример, который бы сделал это:

using (DirectoryEntry entry = new DirectoryEntry())
{
    entry.Username = "here goes the username you want to validate";
    entry.Password = "here goes the password";

    DirectorySearcher searcher = new DirectorySearcher(entry);

    searcher.Filter = "(objectclass=user)";

    try
    {
        searcher.FindOne();
    }
    catch (COMException ex)
    {
        if (ex.ErrorCode == -2147023570)
        {
            // Login or password is incorrect
        }
    }
}

// FindOne() didn't throw, the credentials are correct

Этот код напрямую подключается к серверу AD, используя предоставленные учетные данные. Если учетные данные недействительны, searcher.FindOne () сгенерирует исключение. ErrorCode - это код, соответствующий ошибке COM «неверное имя пользователя / пароль».

Вам не нужно запускать код как пользователь AD. На самом деле, я успешно использую его для запроса информации на сервере AD от клиента за пределами домена!

Матье Гарстецки
источник
как насчет типов аутентификации? Я думаю, что вы забыли это в коде выше. :-) по умолчанию DirectoryEntry.AuthenticationType установлен на Защищенное право? этот код не будет работать на незащищенных LDAP (возможно, Anonymous или None). я правильно с этим?
jerbersoft
Вниз сторона запроса к серверу AD является то , что у вас есть разрешение для запроса к серверу AD. Ваши учетные данные могут быть действительными, но если у вас нет разрешения на запрос AD, вы получите ошибку. Вот почему был создан так называемый Fast Bind ; вы проверяете учетные данные, не давая пользователю возможности что-либо делать.
Ян Бойд
2
Разве это не позволит кому-либо пройти в случае, если COMException выбрасывается по какой-либо другой причине, прежде чем проверяются учетные данные?
Стефан Пол Ноак
11

Еще один вызов .NET для быстрой аутентификации учетных данных LDAP:

using System.DirectoryServices;

using(var DE = new DirectoryEntry(path, username, password)
{
    try
    {
        DE.RefreshCache(); // This will force credentials validation
    }
    catch (COMException ex)
    {
        // Validation failed - handle how you want
    }
}
palswim
источник
Это единственное решение, которое сработало для меня, использование PrincipalContext не сработало для меня.
Даниэль
PrincipalContext недействителен для безопасного соединения LDAP (также известного как LDAPS, который использует порт 636
Kiquenet
10

Попробуйте этот код (ПРИМЕЧАНИЕ. Сообщается, что он не работает на Windows Server 2000)

#region NTLogonUser
#region Direct OS LogonUser Code
[DllImport( "advapi32.dll")]
private static extern bool LogonUser(String lpszUsername, 
    String lpszDomain, String lpszPassword, int dwLogonType, 
    int dwLogonProvider, out int phToken);

[DllImport("Kernel32.dll")]
private static extern int GetLastError();

public static bool LogOnXP(String sDomain, String sUser, String sPassword)
{
   int token1, ret;
   int attmpts = 0;

   bool LoggedOn = false;

   while (!LoggedOn && attmpts < 2)
   {
      LoggedOn= LogonUser(sUser, sDomain, sPassword, 3, 0, out token1);
      if (LoggedOn) return (true);
      else
      {
         switch (ret = GetLastError())
         {
            case (126): ; 
               if (attmpts++ > 2)
                  throw new LogonException(
                      "Specified module could not be found. error code: " + 
                      ret.ToString());
               break;

            case (1314): 
               throw new LogonException(
                  "Specified module could not be found. error code: " + 
                      ret.ToString());

            case (1326): 
               // edited out based on comment
               //  throw new LogonException(
               //   "Unknown user name or bad password.");
            return false;

            default: 
               throw new LogonException(
                  "Unexpected Logon Failure. Contact Administrator");
              }
          }
       }
   return(false);
}
#endregion Direct Logon Code
#endregion NTLogonUser

за исключением того, что вам нужно создать собственное исключение для "LogonException"

Чарльз Бретана
источник
Не используйте обработку исключений для возврата информации из метода. «Неизвестное имя пользователя или неверный пароль» не является исключением, это стандартное поведение для LogonUser. Просто верните ложь.
Треб
да ... это был порт из старой библиотеки VB6 ... написанный 2003 или около того ... (когда впервые вышел .Net)
Чарльз Бретана
При работе в Windows 2000 этот код не будет работать ( support.microsoft.com/kb/180548 )
Ян Бойд,
1
Переосмысливая это. Вход в систему Ожидаемое поведение пользователя, его цель - войти в систему . Если он не в состоянии выполнить эту задачу, это IS исключение. Фактически, метод должен возвращать void, а не Boolean. Кроме того, если вы только что вернули логическое значение, потребитель метода не сможет сообщить пользователю о причине сбоя.
Чарльз Бретана
5

Если вы застряли с .NET 2.0 и управляемым кодом, вот еще один способ, который работает с локальными и доменными учетными записями:

using System;
using System.Collections.Generic;
using System.Text;
using System.Security;
using System.Diagnostics;

static public bool Validate(string domain, string username, string password)
{
    try
    {
        Process proc = new Process();
        proc.StartInfo = new ProcessStartInfo()
        {
            FileName = "no_matter.xyz",
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            UseShellExecute = false,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            RedirectStandardInput = true,
            LoadUserProfile = true,
            Domain = String.IsNullOrEmpty(domain) ? "" : domain,
            UserName = username,
            Password = Credentials.ToSecureString(password)
        };
        proc.Start();
        proc.WaitForExit();
    }
    catch (System.ComponentModel.Win32Exception ex)
    {
        switch (ex.NativeErrorCode)
        {
            case 1326: return false;
            case 2: return true;
            default: throw ex;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return false;
}   
chauwel
источник
Хорошо работает с локальными учетными записями машины, он запускает скрипт
eka808
Кстати, этот метод необходим для того, чтобы сделать это общедоступным. Static SecureString ToSecureString (string PwString) {char [] PasswordChars = PwString.ToCharArray (); SecureString Password = new SecureString (); foreach (char c в PasswordChars) Password.AppendChar (c); ProcessStartInfo foo = new ProcessStartInfo (); foo.Password = Пароль; вернуть foo.Password; }
eka808 29.11.11
Наоборот, в любом случае следует использовать SecureString для паролей. WPF PasswordBox поддерживает это.
Стивен Дрю
5

Проверка подлинности Windows может быть неудачной по разным причинам: неверное имя пользователя или пароль, заблокированная учетная запись, пароль с истекшим сроком действия и многое другое. Чтобы различать эти ошибки, вызовите функцию LogonUser API через P / Invoke и проверьте код ошибки, если функция возвращает false:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

public static class Win32Authentication
{
    private class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle() // called by P/Invoke
            : base(true)
        {
        }

        protected override bool ReleaseHandle()
        {
            return CloseHandle(this.handle);
        }
    }

    private enum LogonType : uint
    {
        Network = 3, // LOGON32_LOGON_NETWORK
    }

    private enum LogonProvider : uint
    {
        WinNT50 = 3, // LOGON32_PROVIDER_WINNT50
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool LogonUser(
        string userName, string domain, string password,
        LogonType logonType, LogonProvider logonProvider,
        out SafeTokenHandle token);

    public static void AuthenticateUser(string userName, string password)
    {
        string domain = null;
        string[] parts = userName.Split('\\');
        if (parts.Length == 2)
        {
            domain = parts[0];
            userName = parts[1];
        }

        SafeTokenHandle token;
        if (LogonUser(userName, domain, password, LogonType.Network, LogonProvider.WinNT50, out token))
            token.Dispose();
        else
            throw new Win32Exception(); // calls Marshal.GetLastWin32Error()
    }
}

Пример использования:

try
{
    Win32Authentication.AuthenticateUser("EXAMPLE\\user", "P@ssw0rd");
    // Or: Win32Authentication.AuthenticateUser("user@example.com", "P@ssw0rd");
}
catch (Win32Exception ex)
{
    switch (ex.NativeErrorCode)
    {
        case 1326: // ERROR_LOGON_FAILURE (incorrect user name or password)
            // ...
        case 1327: // ERROR_ACCOUNT_RESTRICTION
            // ...
        case 1330: // ERROR_PASSWORD_EXPIRED
            // ...
        case 1331: // ERROR_ACCOUNT_DISABLED
            // ...
        case 1907: // ERROR_PASSWORD_MUST_CHANGE
            // ...
        case 1909: // ERROR_ACCOUNT_LOCKED_OUT
            // ...
        default: // Other
            break;
    }
}

Примечание. LogonUser требует доверительных отношений с доменом, с которым вы проверяете.

Майкл Лю
источник
Можете ли вы объяснить, почему ваш ответ лучше, чем ответ с наибольшим количеством голосов?
Мохаммед Али
1
@MohammadAli: Если вам необходимо узнать, почему не удалось проверить учетные данные (неверные учетные данные, заблокированная учетная запись, пароль с истекшим сроком действия и т. Д.), Функция API LogonUser сообщит вам. Напротив, метод PrincipalContext.ValidateCredentials (согласно комментариям к ответу marc_s) не будет; он возвращает false во всех этих случаях. С другой стороны, LogonUser требует доверительных отношений с доменом, а PrincipalContext.ValidateCredentials (я думаю) этого не делает.
Майкл Лю
2

Моя простая функция

 private bool IsValidActiveDirectoryUser(string activeDirectoryServerDomain, string username, string password)
    {
        try
        {
            DirectoryEntry de = new DirectoryEntry("LDAP://" + activeDirectoryServerDomain, username + "@" + activeDirectoryServerDomain, password, AuthenticationTypes.Secure);
            DirectorySearcher ds = new DirectorySearcher(de);
            ds.FindOne();
            return true;
        }
        catch //(Exception ex)
        {
            return false;
        }
    }
Хоссейн Андархора
источник
1

Вот мое полное решение для аутентификации для вашей справки.

Сначала добавьте следующие четыре ссылки

 using System.DirectoryServices;
 using System.DirectoryServices.Protocols;
 using System.DirectoryServices.AccountManagement;
 using System.Net; 

private void AuthUser() { 


      try{
            string Uid = "USER_NAME";
            string Pass = "PASSWORD";
            if (Uid == "")
            {
                MessageBox.Show("Username cannot be null");
            }
            else if (Pass == "")
            {
                MessageBox.Show("Password cannot be null");
            }
            else
            {
                LdapConnection connection = new LdapConnection("YOUR DOMAIN");
                NetworkCredential credential = new NetworkCredential(Uid, Pass);
                connection.Credential = credential;
                connection.Bind();

                // after authenticate Loading user details to data table
                PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
                UserPrincipal user = UserPrincipal.FindByIdentity(ctx, Uid);
                DirectoryEntry up_User = (DirectoryEntry)user.GetUnderlyingObject();
                DirectorySearcher deSearch = new DirectorySearcher(up_User);
                SearchResultCollection results = deSearch.FindAll();
                ResultPropertyCollection rpc = results[0].Properties;
                DataTable dt = new DataTable();
                DataRow toInsert = dt.NewRow();
                dt.Rows.InsertAt(toInsert, 0);

                foreach (string rp in rpc.PropertyNames)
                {
                    if (rpc[rp][0].ToString() != "System.Byte[]")
                    {
                        dt.Columns.Add(rp.ToString(), typeof(System.String));

                        foreach (DataRow row in dt.Rows)
                        {
                            row[rp.ToString()] = rpc[rp][0].ToString();
                        }

                    }  
                }
             //You can load data to grid view and see for reference only
                 dataGridView1.DataSource = dt;


            }
        } //Error Handling part
        catch (LdapException lexc)
        {
            String error = lexc.ServerErrorMessage;
            string pp = error.Substring(76, 4);
            string ppp = pp.Trim();

            if ("52e" == ppp)
            {
                MessageBox.Show("Invalid Username or password, contact ADA Team");
            }
            if ("775​" == ppp)
            {
                MessageBox.Show("User account locked, contact ADA Team");
            }
            if ("525​" == ppp)
            {
                MessageBox.Show("User not found, contact ADA Team");
            }
            if ("530" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this time, contact ADA Team");
            }
            if ("531" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this workstation, contact ADA Team");
            }
            if ("532" == ppp)
            {
                MessageBox.Show("Password expired, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }



        } //common error handling
        catch (Exception exc)
        {
            MessageBox.Show("Invalid Username or password, contact ADA Team");

        }

        finally {
            tbUID.Text = "";
            tbPass.Text = "";

        }
    }
Гаян Чинтхака Дхармаратна
источник