views:

92

answers:

6

Question: I have a document management system, and I am building a Web-Service interfaces to the database.

Everything works so far, just that right now, it's totally unsecured, everybody can access it.

How can I incorporate password or private-public key authentication ?

I can only find 'best practises' and using 'windows user' or passport authentication. But I need authentication from a user and password stored in the database, or better for an RSA private-key stored for each web-service user in the database...

Edit:
I have to use the .NET Framework 2.0 in an ASP.NET environment

+2  A: 

Hi Quandary,

If you are working with WCF, there's an easy way to implement security using X509 certificates. Implementing a binding with security mode 'Message' and clientCredentialType 'Username' it's possible to guarantee this security in a automated way.

The validation can be made through a class wich overrides a method Validate.

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

Jacob84
+1, but unfortunately, I'm limited to ASP.NET with framework 2.0, so no WCF.
Quandary
Ops, I'm lost in that case, I understand that the communication at transport level can be done with X509 certificates also. Maybe the validation process against SQL Server will be more 'manual'. Anyway you can find more info at: http://msdn.microsoft.com/en-us/library/ms996415.aspx
Jacob84
A: 

Have you considered using an external solution for this? My company, WebServius ( http://www.webservius.com ) can help you secure a web service / API, manage its users, measure usage by each user, generate usage reports, etc, etc...

Eugene Osovetsky
+2  A: 

The solution is to write an own http module with a mixture of code provided by MSDN and CodeProject. Including own fixes of MS bugs, and then add this custom soap header to the web service.

<SoapHeader("Authentication", Required:=True)>

This is the module:

Imports System.Web
Imports System.Web.Services.Protocols


' http://msdn.microsoft.com/en-us/library/9z52by6a.aspx
' http://msdn.microsoft.com/en-us/library/9z52by6a(VS.80).aspx




' http://www.codeproject.com/KB/cpp/authforwebservices.aspx


' http://aleemkhan.wordpress.com/2007/09/18/using-wse-30-for-web-service-authentication/
' http://www.codeproject.com/KB/WCF/CustomUserNamePassAuth2.aspx
' http://www.codeproject.com/KB/WCF/CustomUserNamePassAuth2.aspx
' http://www.codeproject.com/KB/webservices/WS-Security.aspx




'Public NotInheritable Class WebServiceAuthenticationModule
Public Class WebServiceAuthenticationModule
    Implements System.Web.IHttpModule

    Protected Delegate Sub WebServiceAuthenticationEventHandler(ByVal sender As [Object], ByVal e As WebServiceAuthenticationEvent)
    Protected _eventHandler As WebServiceAuthenticationEventHandler = Nothing



    Protected Custom Event Authenticate As WebServiceAuthenticationEventHandler
        AddHandler(ByVal value As WebServiceAuthenticationEventHandler)
            _eventHandler = value
        End AddHandler
        RemoveHandler(ByVal value As WebServiceAuthenticationEventHandler)
            _eventHandler = value
        End RemoveHandler
        RaiseEvent(ByVal sender As Object,
                ByVal e As WebServiceAuthenticationEvent)
        End RaiseEvent
    End Event


    Protected app As HttpApplication


    Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
        app = context

        context.Context.Response.Write("<h1>Test</h1>")

        AddHandler app.AuthenticateRequest, AddressOf Me.OnEnter
    End Sub


    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
        ' add clean-up code here if required
    End Sub


    Protected Sub OnAuthenticate(ByVal e As WebServiceAuthenticationEvent)
        If _eventHandler Is Nothing Then
            Return
        End If
        _eventHandler(Me, e)
        If Not (e.User Is Nothing) Then
            e.Context.User = e.Principal
        End If

    End Sub 'OnAuthenticate 


    Public ReadOnly Property ModuleName() As String
        Get
            Return "WebServiceAuthentication"
        End Get
    End Property


    Sub OnEnter(ByVal [source] As [Object], ByVal eventArgs As EventArgs)
        'Dim app As HttpApplication = CType([source], HttpApplication)
        'app = CType([source], HttpApplication)
        Dim context As HttpContext = app.Context
        Dim HttpStream As System.IO.Stream = context.Request.InputStream

        ' Save the current position of stream.
        Dim posStream As Long = HttpStream.Position

        ' If the request contains an HTTP_SOAPACTION 
        ' header, look at this message.

        'For Each str As String In context.Request.ServerVariables.AllKeys

        'If context.Request.ServerVariables(Str) IsNot Nothing Then
        'context.Response.Write("<h1>" + Str() + "= " + context.Request.ServerVariables(Str) + "</h1>")
        'End If
        'Next
        If context.Request.ServerVariables("HTTP_SOAPACTION") Is Nothing Then
            'context.Response.End()
            Return
            'Else
            'MsgBox(New System.IO.StreamReader(context.Request.InputStream).ReadToEnd())
        End If


        ' Load the body of the HTTP message
        ' into an XML document.
        Dim dom As New System.Xml.XmlDocument()
        Dim soapUser As String
        Dim soapPassword As String

        Try
            dom.Load(HttpStream)

            'dom.Save("C:\Users\Administrator\Desktop\SoapRequest.xml")
            ' Reset the stream position.
            HttpStream.Position = posStream

            ' Bind to the Authentication header.
            soapUser = dom.GetElementsByTagName("Username").Item(0).InnerText
            soapPassword = dom.GetElementsByTagName("Password").Item(0).InnerText
        Catch e As Exception
            ' Reset the position of stream.
            HttpStream.Position = posStream

            ' Throw a SOAP exception.
            Dim name As New System.Xml.XmlQualifiedName("Load")
            Dim ssoapException As New SoapException("Unable to read SOAP request", name, e)
            context.Response.StatusCode = System.Net.HttpStatusCode.Unauthorized
            context.Response.StatusDescription = "Access denied."

            ' context.Response.Write(ssoapException.ToString())
            'Dim x As New System.Xml.Serialization.XmlSerializer(GetType(SoapException))
            'context.Response.ContentType = "text/xml"
            'x.Serialize(context.Response.OutputStream, ssoapException)


            'Throw ssoapException

            context.Response.End()
        End Try

        ' Raise the custom global.asax event.
        OnAuthenticate(New WebServiceAuthenticationEvent(context, soapUser, soapPassword))
        Return
    End Sub 'OnEnter


End Class ' WebServiceAuthenticationModule
Quandary
A: 

Before you roll your own authentication, you might want to have a look at Web Services Enhancements (WSE) 2.0 SP2 for Microsoft .NET. It's an implementation of the WS-Security specification for .net.

google wse 2.0 or WS-Security for more links.

Robert Paulson
+1  A: 

If you are still using an ASP.NET SOAP web service then the easiest way that fits your requirements IMO is to use the ASP.NET Forms authentication with a Membership DB. If you are starting out fresh I'd recommend going with WCF - if you can't/or won't do that this post applies to the "classic" ASP.NET SOAP web services.

To add Forms authentication to a web service:

  1. Configure it just like you would for any other web site but set it to allow access for everyone:

    <authorization>
        <allow users="*"/>
    </authorization>
    
  2. Implement Login/Logout methods and issue the authentication ticket in the Login method. Further requests to the web service then can use the issued authentication ticket.

  3. All other web methods you want to protect you can then decorate with

    [PrincipalPermission(SecurityAction.Demand, Authenticated = true)]

These methods will now throw a Security exception if a client is not authenticated.

Example for a protected method:

[PrincipalPermission(SecurityAction.Demand, Authenticated = true)]
[WebMethod(Description = "Your protected method")]
public string Foo()
{
    return "bar";
}

Example for Login method:

[WebMethod(Description = "Login to start a session")]
public bool Login(string userName, string password)
{
    if (!Membership.Provider.ValidateUser(userName, password))
        return false;

    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
           1,
           userName,
           DateTime.Now,
           DateTime.Now.AddMinutes(500),
           false,
           FormsAuthentication.FormsCookiePath);// Path cookie valid for

    // Encrypt the cookie using the machine key for secure transport
    string hash = FormsAuthentication.Encrypt(ticket);
    HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, // Name of auth cookie
                                       hash); // Hashed ticket

    // Set the cookie's expiration time to the tickets expiration time
    if (ticket.IsPersistent)
        cookie.Expires = ticket.Expiration;

    // Add the cookie to the list for outgoing response
    if(HttpContext.Current !=null)
        HttpContext.Current.Response.Cookies.Add(cookie);

    FormsAuthentication.SetAuthCookie(userName, true);
    return true;
}
BrokenGlass
And then you will have to call login and logout each time you want to use the web service, with blackbox transmission system, and undefined behaviour if client or server crashes in between. And whoe if you forget to add PrincipalPermission to one single method... Nono, anything that wants to be serious must deny access by default, and just take the login data as a service parameter, and not as method.
Quandary
It's no different than regular UI Forms authentication, still easier to get right with the disadvantages mentioned than rolling your own. Plus you get Roles/Membership features for free. Point taken on secure by default though.
BrokenGlass
A: 

If your WS is going to be consumed through SOAP protocol, the you can implement the Security through the SOAP Header:

using System.Web.Services;
using System.Web.Services.Protocols;

namespace Domain.WS
{
    [Serializable]
    public class SoapWSHeader : System.Web.Services.Protocols.SoapHeader, ISoapWSHeader
    {
        public string UserId { get; set; }
        public string ServiceKey { get; set; }
        public ApplicationCode ApplicationCode { get; set; }        
    }    

    [WebService(Namespace = "http://domain.some.unique/")]        
    public class MyServices : System.Web.Services.WebService
    {
        public SoapWSHeader WSHeader;
        private ServicesLogicContext _logicServices;

        public MyServices() { _logicServices = new ServicesLogicContext(new LogicInfo() {...}); }

        [WebMethod, SoapHeader("WSHeader", Direction = SoapHeaderDirection.InOut)]
        public Result WSMethod1(Int32 idSuperior)
        {
            _logicServices.ThrowIfNotAuthenticate(WSHeader); 
            return _logicServices.WSMethod1(idSuperior) as Result;
        }
    }
}

namespace Domain.Logic 
{
    [Serializable]    
    public class ServicesLogicContext : ServicesLogicContextBase
    {
        protected ISoapWSHeader SoapWSHeader { get; set; }
        public ServicesLogicContext(LogicInfo info) : base(info) {}

        public IResult WSMethod1(Int32 idSuperior)
        {
            IResult result = null; 
            //-- method implementation here...
            return result;
        }

        public void ThrowIfNotAuthenticate(ISoapWSHeader soapWSHeader) {
            this.SoapWSHeader = soapWSHeader;
            if (SoapWSHeader != null)
            {
                if (!ValidateCredentials(soapWSHeader))
                {
                    throw new System.Security.SecurityException(Resources.ValidationErrorWrongCredentials);
                }
            }
            else { throw new System.Security.SecurityException(Resources.ValidationErrorWrongWSHeader); }
        }
        private bool ValidateCredentials(ISoapWSHeader soapWSHeader) {   
            return (SoapWSHeader.UserId.Equals("USER_ID") && SoapWSHeader.ServiceKey.Equals("PSW_1"));
        }
    }
}

Note: this code is not complete, this only depicts the main aspects on how to use the SOAP Header.

ArceBrito
If you had paid attention, then you'd have realized that this is exactly what I have done. Just that I have moved the authentication from incorporating into every WebMethod, which is bs, to said HTTP module. With the added bonus that it denies access to every SOAP call without header.
Quandary
Ok, it means you have solved your problem. Regards, we are applying that technique to secure some WS, also, depending of your application you could use some certificate through HTTPS.
ArceBrito