/// <summary> /// Authenticates the username and password against the RADIUS server. /// </summary> /// <param name="username">Username to be authenticated.</param> /// <param name="password">Password to be authenticated.</param> /// <param name="state">State value from a previous challenge response.</param> /// <returns>Response packet received from the server for the authentication request.</returns> /// <remarks> /// <para> /// The type of response packet (if any) will be one of the following: /// <list> /// <item>AccessAccept: If the authentication is successful.</item> /// <item>AccessReject: If the authentication is not successful.</item> /// <item>AccessChallenge: If the server need more information from the user.</item> /// </list> /// </para> /// <para> /// When an AccessChallenge response packet is received from the server, it contains a State attribute /// that must be included in the AccessRequest packet that is being sent in response to the AccessChallenge /// response. So if this method returns an AccessChallenge packet, then this method is to be called again /// with the requested information (from ReplyMessage attribute) in the password field and the value State /// attribute. /// </para> /// </remarks> public RadiusPacket Authenticate(string username, string password, byte[] state) { CheckDisposed(); if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) { throw new ArgumentException("Username and Password cannot be null."); } RadiusPacket request = new RadiusPacket(PacketType.AccessRequest); byte[] authenticator = RadiusPacket.CreateRequestAuthenticator(m_sharedSecret); request.Authenticator = authenticator; request.Attributes.Add(new RadiusPacketAttribute(AttributeType.UserName, username)); request.Attributes.Add(new RadiusPacketAttribute(AttributeType.UserPassword, RadiusPacket.EncryptPassword(password, m_sharedSecret, authenticator))); // State attribute is used when responding to a AccessChallenge response. if ((object)state != null) { request.Attributes.Add(new RadiusPacketAttribute(AttributeType.State, state)); } return(ProcessRequest(request)); }
/// <summary> /// Send a request to the server and waits for a response back. /// </summary> /// <param name="request">Request to be sent to the server.</param> /// <returns>Response packet if a valid response is received from the server; otherwise Nothing.</returns> public RadiusPacket ProcessRequest(RadiusPacket request) { CheckDisposed(); // We wait indefinitely for the connection to establish. But since this is UDP, the connection // will always be successful (locally we're binding to any available UDP port). if (m_udpClient.CurrentState != ClientState.Connected) { return(null); } RadiusPacket response = null; // We have a UDP socket we can use for exchanging packets. DateTime stopTime; for (int i = 1; i <= m_requestAttempts; i++) { m_responseBytes = null; m_udpClient.Send(request.BinaryImage()); stopTime = DateTime.UtcNow.AddMilliseconds(m_reponseTimeout); while (true) { Thread.Sleep(1); // Stay in the loop until: // 1) We receive a response OR // 2) We exceed the response timeout duration if ((object)m_responseBytes != null || DateTime.UtcNow > stopTime) { break; } } if ((object)m_responseBytes != null) { // The server sent a response. response = new RadiusPacket(m_responseBytes, 0, m_responseBytes.Length); if (response.Identifier == request.Identifier && response.Authenticator.CompareTo(RadiusPacket.CreateResponseAuthenticator(m_sharedSecret, request, response)) == 0) { break; } // The response failed the verification, so we'll silently discard it. response = null; } } return(response); }
/// <summary> /// Generates an "Authenticator" value used in a RADIUS response packet sent by the server to client. /// </summary> /// <param name="sharedSecret">The shared secret key.</param> /// <param name="requestPacket">RADIUS packet sent from client to server.</param> /// <param name="responsePacket">RADIUS packet sent from server to client.</param> /// <returns>A byte array.</returns> public static byte[] CreateResponseAuthenticator(string sharedSecret, RadiusPacket requestPacket, RadiusPacket responsePacket) { // Response authenticator is generated as follows: // MD5(Code + Identifier + Length + Request Authenticator + Attributes + Shared Secret) // where: // Code, Identifier, Length & Attributes are from the response RADIUS packet // Request Authenticator is from the request RADIUS packet // Shared Secret is the shared secret key int length = responsePacket.BinaryLength; byte[] sharedSecretBytes = Encoding.GetBytes(sharedSecret); byte[] buffer = new byte[length + sharedSecretBytes.Length]; responsePacket.GenerateBinaryImage(buffer, 0); Buffer.BlockCopy(requestPacket.BinaryImage(), 4, buffer, 4, 16); Buffer.BlockCopy(sharedSecretBytes, 0, buffer, length, sharedSecretBytes.Length); return(new MD5CryptoServiceProvider().ComputeHash(buffer)); }
/// <summary> /// Determines whether or not the response indicates that the user account is in "Next Token" mode. /// </summary> /// <param name="response">Response packet sent by the server.</param> /// <returns>True if the user account is in "Next Token" mode; otherwise False.</returns> /// <remarks> /// <para> /// A user's account can enter the "Next Token" mode after the user enters incorrect passwords for a few /// times (3 times by default) and then enters the correct password. Note that repeatedly entering /// incorrect passwords will disable the user account. /// </para> /// <para>NOTE: This method is specific to RSA RADIUS implementation.</para> /// </remarks> public bool IsUserInNextTokenMode(RadiusPacket response) { CheckDisposed(); if ((object)response == null) { throw new ArgumentNullException(nameof(response)); } byte[] messageBytes = response.GetAttributeValue(AttributeType.ReplyMessage); if ((object)messageBytes == null) { throw new ArgumentException("ReplyMessage attribute is not present", nameof(response)); } // Unfortunately, the only way of determining whether or not a user account is in the // "Next Token" mode is from the text present in the ReplyMessage attribute of the // AccessChallenge response from server. string messageString = RadiusPacket.Encoding.GetString(messageBytes, 0, messageBytes.Length); return(messageString.ToLower().Contains(m_nextTokenModeMessage.ToLower())); }
/// <summary> /// Determines whether or not the response indicates that the user account is in "Next Token" mode. /// </summary> /// <param name="response">Response packet sent by the server.</param> /// <returns>True if the user account is in "Next Token" mode; otherwise False.</returns> /// <remarks> /// <para> /// A user's account can enter the "Next Token" mode after the user enters incorrect passwords for a few /// times (3 times by default) and then enters the correct password. Note that repeatedly entering /// incorrect passwords will disable the user account. /// </para> /// <para>NOTE: This method is specific to RSA RADIUS implementation.</para> /// </remarks> public bool IsUserInNextTokenMode(RadiusPacket response) { CheckDisposed(); if ((object)response == null) throw new ArgumentNullException("response"); byte[] messageBytes = response.GetAttributeValue(AttributeType.ReplyMessage); if ((object)messageBytes == null) throw new ArgumentException("ReplyMessage attribute is not present", "response"); // Unfortunately, the only way of determining whether or not a user account is in the // "Next Token" mode is from the text present in the ReplyMessage attribute of the // AccessChallenge response from server. string messageString = RadiusPacket.Encoding.GetString(messageBytes, 0, messageBytes.Length); return messageString.ToLower().Contains(m_nextTokenModeMessage.ToLower()); }
/// <summary> /// Authenticates the username and password against the RADIUS server. /// </summary> /// <param name="username">Username to be authenticated.</param> /// <param name="password">Password to be authenticated.</param> /// <param name="state">State value from a previous challenge response.</param> /// <returns>Response packet received from the server for the authentication request.</returns> /// <remarks> /// <para> /// The type of response packet (if any) will be one of the following: /// <list> /// <item>AccessAccept: If the authentication is successful.</item> /// <item>AccessReject: If the authentication is not successful.</item> /// <item>AccessChallenge: If the server need more information from the user.</item> /// </list> /// </para> /// <para> /// When an AccessChallenge response packet is received from the server, it contains a State attribute /// that must be included in the AccessRequest packet that is being sent in response to the AccessChallenge /// response. So if this method returns an AccessChallenge packet, then this method is to be called again /// with the requested information (from ReplyMessage attribute) in the password field and the value State /// attribute. /// </para> /// </remarks> public RadiusPacket Authenticate(string username, string password, byte[] state) { CheckDisposed(); if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) throw new ArgumentException("Username and Password cannot be null."); RadiusPacket request = new RadiusPacket(PacketType.AccessRequest); byte[] authenticator = RadiusPacket.CreateRequestAuthenticator(m_sharedSecret); request.Authenticator = authenticator; request.Attributes.Add(new RadiusPacketAttribute(AttributeType.UserName, username)); request.Attributes.Add(new RadiusPacketAttribute(AttributeType.UserPassword, RadiusPacket.EncryptPassword(password, m_sharedSecret, authenticator))); // State attribute is used when responding to a AccessChallenge response. if ((object)state != null) request.Attributes.Add(new RadiusPacketAttribute(AttributeType.State, state)); return ProcessRequest(request); }
/// <summary> /// Send a request to the server and waits for a response back. /// </summary> /// <param name="request">Request to be sent to the server.</param> /// <returns>Response packet if a valid response is received from the server; otherwise Nothing.</returns> public RadiusPacket ProcessRequest(RadiusPacket request) { CheckDisposed(); // We wait indefinitely for the connection to establish. But since this is UDP, the connection // will always be successful (locally we're binding to any available UDP port). if (m_udpClient.CurrentState != ClientState.Connected) return null; RadiusPacket response = null; // We have a UDP socket we can use for exchanging packets. DateTime stopTime; for (int i = 1; i <= m_requestAttempts; i++) { m_responseBytes = null; m_udpClient.Send(request.BinaryImage()); stopTime = DateTime.UtcNow.AddMilliseconds(m_reponseTimeout); while (true) { Thread.Sleep(1); // Stay in the loop until: // 1) We receive a response OR // 2) We exceed the response timeout duration if ((object)m_responseBytes != null || DateTime.UtcNow > stopTime) break; } if ((object)m_responseBytes != null) { // The server sent a response. response = new RadiusPacket(m_responseBytes, 0, m_responseBytes.Length); if (response.Identifier == request.Identifier && response.Authenticator.CompareTo(RadiusPacket.CreateResponseAuthenticator(m_sharedSecret, request, response)) == 0) break; // The response failed the verification, so we'll silently discard it. response = null; } } return response; }
/// <summary> /// Generates an "Authenticator" value used in a RADIUS response packet sent by the server to client. /// </summary> /// <param name="sharedSecret">The shared secret key.</param> /// <param name="requestPacket">RADIUS packet sent from client to server.</param> /// <param name="responsePacket">RADIUS packet sent from server to client.</param> /// <returns>A byte array.</returns> public static byte[] CreateResponseAuthenticator(string sharedSecret, RadiusPacket requestPacket, RadiusPacket responsePacket) { // Response authenticator is generated as follows: // MD5(Code + Identifier + Length + Request Authenticator + Attributes + Shared Secret) // where: // Code, Identifier, Length & Attributes are from the response RADIUS packet // Request Authenticator is from the request RADIUS packet // Shared Secret is the shared secret key int length = responsePacket.BinaryLength; byte[] sharedSecretBytes = Encoding.GetBytes(sharedSecret); byte[] buffer = new byte[length + sharedSecretBytes.Length]; responsePacket.GenerateBinaryImage(buffer, 0); Buffer.BlockCopy(requestPacket.BinaryImage(), 4, buffer, 4, 16); Buffer.BlockCopy(sharedSecretBytes, 0, buffer, length, sharedSecretBytes.Length); return new MD5CryptoServiceProvider().ComputeHash(buffer); }