2008年7月31日

[IIS][ASP.Net][Web Service][WSE] Web service and Forms authentication - 2

Second solution for Forms Authentication with web service.

  1. Create a new project.
  2. Create a new class implements IHttpModule interface, attach HttpApplication::AuthenticateRequest event in Init() function -
  3. context.AuthenticateRequest += new EventHandler(context_AuthenticateRequest);



  4. In context_AuthenticateRequest(), we check if this is a request to our secured web service, if so, we issue a HTTP 401 to the client, this will ask client to request web service again with WWW-Authentication header present if client has set PreAuthenticate to true.(and if not, client end up with a HTTP 401 error)


  5. private bool IsAuthenticationRequired(HttpApplication app)
    {
    return app.Context.Request.Url.AbsolutePath.EndsWith("someservice.asmx", StringComparison.OrdinalIgnoreCase);
    }


    private void context_AuthenticateRequest(object sender, EventArgs e){
    if(!IsAuthenticationRequired((HttpApplication)sender))
    return;
    if (string.IsNullOrEmpty(httpApplication.Request.Headers["Authorization"]))
    {
    //client have to make a Basic authorization communication with server
    requestBasicAuthentication(httpApplication);//Request authorization
    }else{//...Authenticate user here...}
    }


    private void requestBasicAuthentication(HttpApplication httpApplication)
    {
    httpApplication.Response.AppendHeader(
    "WWW-Authenticate",
    string.Format("Basic realm=\"{0}\"",
    "my service test"));
    httpApplication.Response.StatusCode = 401;
    httpApplication.CompleteRequest();
    }



  6. Now we have to make this request authenticated, since our web site is using Forms Authentication, we call Membership.ValidateUser() to validate the incoming request as web site do. If the user is validated, don't forget to set user identity to the request.


  7. //...
    //check to see if is Basic authentication we are facing
    string sToken = httpApplication.Request.Headers["Authorization"];
    if (sToken.StartsWith("Basic", StringComparison.OrdinalIgnoreCase))
    {
    //Basic ahthorization
    validateBasicAuthentication(httpApplication, sToken);
    }
    //...
    private void validateBasicAuthentication(HttpApplication httpApplication, string token)
    {
    string credentialString = token.Substring(6);
    byte[] buffer = Convert.FromBase64String(credentialString);
    credentialString = Encoding.Default.GetString(buffer);
    string[] frags = credentialString.Split(new char[] { ':' }, 2);
    if (!System.Web.Security.Membership.ValidateUser(frags[0], frags[1]))
    DenyAccess(httpApplication);//bad identity
    else{
    //This is required so that web application can get user identity via User property
    SetPrinciple(httpApplication, frags[0]);
    }
    }



  8. To deny an acccess, simply issue a HTTP 401 response, and to set principal, we modify current context's Principal property.


  9. private void SetPrinciple(HttpApplication httpApplication, string userName)
    {
    RolePrincipal principal = new RolePrincipal(new System.Web.Security.FormsIdentity(
    new FormsAuthenticationTicket(userName, false, 10)
    ));
    httpApplication.Context.User = principal;
    }



  10. Now we have to make some changes to web.config, first, ofcourse, we have to register our http module.


  11.     <httpModules>
    <add name="mymodule" type="MyModule,MyAuthention"></add>
    </httpModules>



  12. This register our http module, but it's not just enough, because by default, FormsAuthentication module fires before modules listed under <httpModules> element, and if we have to authenticate incoming request before all other authentication modules. To do this, we can simply remove FormsAuthentication module by adding a <remove name="FormsAuthentication"/> before the <add> element.


  13. <remove name="FormsAuthentication"></remove>
    <add name="mymodule" type="MyModule,MyAuthention"></add>



  14. But, remember, our module only authenticate request to web services, not classic web pages, so we must add FormsAuthentication back again.


  15. <remove name="FormsAuthentication"></remove>
    <add name="mymodule" type="MyModule,MyAuthention"></add>
    <add name="FormsAuthenticationOld" type="System.Web.Security.FormsAuthenticationModule"/>



  16. And here's how client calls web service


  17. ws.CookieContainer = cc;//cookie container
    //set PreAuthenticate = true, so when a HTTP 401 received,
    //client will issue another request with authentication headers
    ws.PreAuthenticate = true;

    ws.Credentials = new System.Net.NetworkCredential(user, password);





2008年7月25日

[IIS][ASP.Net][Web Service][WSE] Web service and Forms authentication

We have a ASP.Net 2.0 web site secured by Forms authentication with custom membership provider & role manager, within this web site, we have a web service exposed as an API. We want this web service also be secured with same mechanism.

I thought of adding Credentials to web service proxy class, but this fails since Credentials should only work with Windows authentication.

Then I thought of maybe <location/> element helps since this element enables you have different configuration settings with different urls. and this also failed, because you can't write a web.config like this:

  <location path="." allowOverride="true" inheritInChildApplications="true">
<system.web>
<authentication mode="Forms">
<forms loginUrl="login.aspx" name=".ASPXFORMSAUTH">
</forms>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
<location path="svc.asmx">
<system.web>
<forms loginUrl="svc.asmx?op=Logon" name=".ASPXFORMSAUTH">
</forms>
</system.web>
</location>


And this fails still because <authentication/> is not a overridable element in web.config, so that you can not specify 2 different login pages in same web site.



Finally, Andrew Arnott's Blog saves me, he has the same issue I have, follow his instruction, we are now able to use same Membership provider/Role manager to authenticate both web site & web service.



Bellow is a summary:




  1. Install WSE 3.0


  2. On your web project, open up WSE 3.0 property page

    1. Check both [Enable this project for web services Enhancements] and [Enable Microsoft web services Enhacement Soap protocol Factory]


    2. Under [Security] tab, add a custom token manager


    3. Enable policy, name your new policy, this policy name will be used both server side and client side, so don't miss it.




  3. Click OK to complete server setting.

    1. a wse3policyCache.config file will be generated




  4. Enable your web site's Membership/RoleManager if they are not enabled yet.


  5. Modify wse3policyCache.config, allow specificed roles to access this web service(for me,"User" role are allowed), your wse3policyCache.config should look like this
    <policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
    <extensions>
    <extension name="usernameOverTransportSecurity" type="Microsoft.Web.Services3.Design.UsernameOverTransportAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <extension name="requireActionHeader" type="Microsoft.Web.Services3.Design.RequireActionHeaderAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </extensions>
    <policy name="usernameTokenSecurity">
    <authorization>
    <allow role="User"/>
    <deny role="*"/>
    </authorization>
    <usernameOverTransportSecurity />
    <requireActionHeader />
    </policy>
    </policies>



  6. Add a custom token manager in APP_CODE folder, code listed bellow:
    using System.Xml;
    using System.Web.Security;
    using System.Security.Permissions;
    using System.Security.Principal;
    using Microsoft.Web.Services3.Security;
    using Microsoft.Web.Services3.Security.Tokens;

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public class CustomUsernameTokenManager : UsernameTokenManager
    {
    //codes omitted.
    protected override string AuthenticateToken(UsernameToken token)
    {
    bool validCredentials = Membership.ValidateUser(token.Username, token.Password);
    if (!validCredentials) throw new UnauthorizedAccessException();

    GenericIdentity identity = new GenericIdentity(token.Username);
    GenericPrincipal principal = new GenericPrincipal(identity, Roles.GetRolesForUser(token.Username));
    token.Principal = principal;

    return token.Password;
    }
    }



  7. Modify <microsoft.web.services3> section in your web site's web.config, should look like this:
      <microsoft.web.services3>
    <security>
    <securityTokenManager>
    <add
    type="CustomUsernameTokenManager, __code"
    namespace="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" localName="UsernameToken" />
    </securityTokenManager>
    </security>
    <policy fileName="wse3policyCache.config" />
    </microsoft.web.services3>




  8. Modify web.config to allow client to call your web service


      <location path="svc.asmx">
    <system.web>
    <authorization>
    <allow users="*"></allow>
    </authorization>
    </system.web>
    </location>




Now it's time to configure your client application.





    1. open WSE property page on client project


      1. Check [Enable this project for Web Service Enhancements]


      2. Enable Policy, add a new policy with exactly same name you named your server policy, in our case, should be "usernameTokenSecurity"

        1. A wse3policyCache.config file will be generated




      3. Update your web reference


      4. Modify your client code as follow:
              private GetService CreateWebService()
        {
        if (ws == null)
        ws = new svcWse();
        ws = new svcWse();

        UsernameToken userName = new UsernameToken(user, password,PasswordOption.SendPlainText);
        ws.SetClientCredential(userName);
        ws.SetPolicy("usernameTokenSecurity");
        return ws;
        }
        public String[] GetItemList()
        {
        return CreateWebService().GetItemList();
        }







N

2008年7月10日

[BizTalk] BTS stop responding and you got TDDS lock timeout error in event log

My BTS 2006 stop processing incoming messages with the following event log entry.

Event Type:    Error
Event Source: BAM EventBus Service
Event Category: None
Event ID: 25
Date: 7/10/2008
Time: 5:51:49 PM
User: N/A
Computer: [BTSSVC]


Description:
Either another TDDS is processing the same
data or there is an orphaned session in SQL
server holding TDDS lock.Timeout expired.
The timeout period elapsed prior to
completion of the operation or the server
is not responding.
SQLServer: [SQLSVRNAME],
Database: BAMPrimaryImport.


For more information
, see Help and Support Center at
http://go.microsoft.com/fwlink/events.asp.


If you are running BTS 2004, then simply following this KB article (Microsoft KB897653).



If you are running BTS 2006, TIHO's blog has detail information about this error and how to fix it.



Simply speaking, if this happens on BTS2006 server, then one of the following must be solved:




  1. There are orphanded sessions.

    1. Orphanded sessions are those clients that is not able to free their network connection when terminated.


    2. This article shows how to identify if you are running into orphanded session problems and how to solve it.




  2. You have permission issues

    1. The account under which TDDS(Tracking Data Decode Service) must have execute permissions for the following stored procedures in BTS Msg db:TDDS_RedisterTDDSAccess & TDDS_GetNumTrackingPartitions.


    2. The same account must also have execute permission to TDDS_Lock stored procedure in DTA/HAT & BAM Primary Import Databases.


        1. To check and solve permission issues, first connect to BTS Msg DB with same credentials TDDS is running under. then try to execute the following sql statement:
          use [BizTalkMsgBoxDb]

          DECLARE @RC int
          DECLARE @retVal int
          EXEC @RC =
          [dbo].[TDDS_RegisterTDDSAccess]
          @retVal OUTPUT
          SELECT @RC
          GO

          DECLARE @RC int
          DECLARE @nPartitions tinyint
          EXEC @RC =
          [dbo].[TDDS_GetNumTrackingPartitions]
          @nPartitions OUTPUT
          SELECT @RC
          GO





      1. And the following against BTS DTA & BAM Primary Import databases:



        use [BAMPrimaryImport].
        DECLARE @RC int
        DECLARE @resource nvarchar(128)
        DECLARE @milisecTimeout int
        DECLARE @retVal int
        SELECT @resource = N'Foo'
        SELECT @milisecTimeout = 5000
        EXEC @RC =
        [dbo].[TDDS_Lock]
        @resource,
        @milisecTimeout,
        @retVal OUTPUT
        SELECT @retVal
        SELECT @RC
        GO









If you got error messages state you are lack of permissions executing those sql statement against BTS MsgDB & DTA/BAM DBs, try to grant execute permission to the account you are connecting to SQL server until no more error messages are shown.

Blog Archive

About Me