1

I am working on WCF Rest application I need to implement the token based authentication in it.Please suggest me a perfect way to implement the token based authentication WCF Rest.

ARUNRAJ
  • 469
  • 6
  • 15

2 Answers2

3

I was able to implement AAD Token based authentication in a WCF based SOAP service.

For this I leveraged WCF extensibility features - Message Inspector and Custom Invoker in the following way

  1. Message Inspector : Using the message inspector, we extract the Bearer Token from the Authorization Header of the incoming request. Post this we perform the token validation using OIDC library to get the keys and config for Microsoft AAD. If token is validated, the operation is invoked and we get the response on the client side.

    If the Token validation fails, we stop the request processing by using Custom Invoker and return 401 Unauthorized response to the caller with a custom error message.

public class BearerTokenMessageInspector : IDispatchMessageInspector
{
    /// Method called just after request is received. Implemented by default as defined in IDispatchMessageInspector
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        WcfErrorResponseData error = null;
        var requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
        if (request == null)
        {
            error = new WcfErrorResponseData(HttpStatusCode.BadRequest, string.Empty, new KeyValuePair<string, string>("InvalidOperation", "Request Body Empty."));
            return error;
        }
        var authHeader = requestMessage.Headers["Authorization"];
        try
        {
            if (string.IsNullOrEmpty(authHeader))
            {
                error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Error: Authorization Header empty! Please pass a Token using Bearer scheme."));
            }
            else if (this.Authenticate(authHeader))
            {
                return null;
            }
        }
        catch (Exception e)
        {
            error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Token with Client ID \"" + clientID + "\" failed validation with Error Messsage - " + e.Message));
        }

        if (error == null) //Means the token is valid but request must be unauthorized due to not-allowed client id
        {
            error = new WcfErrorResponseData(HttpStatusCode.Unauthorized, string.Empty, new KeyValuePair<string, string>("WWW-Authenticate", "Token with Client ID \"" + clientID + "\" failed validation with Error Messsage - " + "The client ID: " + clientID + " might not be in the allowed list."));
        }

        //This will be checked before the custom invoker invokes the method, if unauthorized, nothing is invoked
        OperationContext.Current.IncomingMessageProperties.Add("Authorized", false);
        return error;
    }

    /// Method responsible for validating the token and tenantID Claim. 
    private bool Authenticate(string authHeader)
    {
        const string bearer = "Bearer ";
        if (!authHeader.StartsWith(bearer, StringComparison.InvariantCultureIgnoreCase)) { return false; }
        var jwtToken = authHeader.Substring(bearer.Length);
        PopulateIssuerAndKeys();
        var validationParameters = GenerateTokenValidationParameters(_signingKeys, _issuer);
        return ValidateToken(jwtToken, validationParameters);
    }

    /// Method responsible for validating the token against the validation parameters. Key Rollover is 
    /// handled by refreshing the keys if SecurityTokenSignatureKeyNotFoundException is thrown.
    private bool ValidateToken(string jwtToken, TokenValidationParameters validationParameters)
    {
        int count = 0;
        bool result = false;
        var tokenHandler = new JwtSecurityTokenHandler();
        var claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out SecurityToken validatedToken);
        result = (CheckTenantID(validatedToken));
        return result;
    }

    /// Method responsible for sending proper Unauthorized reply if the token validation failed. 
    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        var error = correlationState as WcfErrorResponseData;
        if (error == null) return;
        var responseProperty = new HttpResponseMessageProperty();
        reply.Properties["httpResponse"] = responseProperty;
        responseProperty.StatusCode = error.StatusCode;
        var headers = error.Headers;
        if (headers == null) return;
        foreach (var t in headers)
        {
            responseProperty.Headers.Add(t.Key, t.Value);
        }
    }
}

NOTE - Please refer to this gist for complete Message Inspector code.

  1. Custom Invoker - The job of custom Invoker is to stop the request flow to further stages of WCF Request processing pipeline if the Token is invalid. This is done by setting the Authorized flag as false in the current OperationContext (set in Message Inspector) and reading the same in Custom Invoker to stop the request flow.
class CustomInvoker : IOperationInvoker
{
    public object Invoke(object instance, object[] inputs, out object[] outputs)
    {
        // Check the value of the Authorized header added by Message Inspector
        if (OperationContext.Current.IncomingMessageProperties.ContainsKey("Authorized"))
        {
            bool allow = (bool)OperationContext.Current.IncomingMessageProperties["Authorized"];
            if (!allow)
            {
                outputs = null;
                return null;
            }
        }
        // Otherwise, go ahead and invoke the operation
        return defaultInvoker.Invoke(instance, inputs, out outputs);
    }
}

Here's the complete gist for Custom Invoker.

Now you need to inject the Message Inspector and the Custom Invoker to your WCF Pipeline using Endpoint Behavior Extension Element. Here are the gists for the class files to do this and a few other needed helper classes:

  1. BearerTokenEndpointBehavior
  2. BearerTokenExtensionElement
  3. MyOperationBehavior
  4. OpenIdConnectCachingSecurityTokenProvider
  5. WcfErrorResponseData

    The job is not done yet!
    You need to write a custom binding and expose a custom AAD endpoint in your web.config apart from adding the AAD Config Keys -
<!--List of AAD Settings-->
<appSettings>
    <add key="AADAuthority" value="https://login.windows.net/<Your Tenant ID>"/>
    <add key="AADAudience" value="your service side AAD App Client ID"/>
    <add key="AllowedTenantIDs" value="abcd,efgh"/>
    <add key="ValidateIssuer" value="true"/>
    <add key="ValidateAudience" value="true"/>
    <add key="ValidateIssuerSigningKey" value="true"/>
    <add key="ValidateLifetime" value="true"/>
    <add key="useV2" value="true"/>
    <add key="MaxRetries" value="2"/>
</appSettings>

<bindings>
  <wsHttpBinding>
    <!--wsHttpBinding needs client side AAD Token-->
    <binding name="wsHttpBindingCfgAAD" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00" sendTimeout="00:30:00">
      <readerQuotas maxDepth="26214400" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
      <security mode="Transport">
        <transport clientCredentialType="None"/>
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

<services>
  <!--Exposing a new baseAddress/wssecureAAD endpoint which will support AAD Token Validation-->
  <service behaviorConfiguration="ServiceBehaviorCfg" name="Service">
    <!--wshttp endpoint with client AAD Token based security-->
    <endpoint address="wsSecureAAD" binding="wsHttpBinding" bindingConfiguration="wsHttpBindingCfgAAD" name="ServicewsHttpEndPointAAD" contract="ServiceContracts.IService" behaviorConfiguration="AADEnabledEndpointBehavior"/>
  </service>
</services>

<behaviors>
  <endpointBehaviors> <!--Injecting the Endpoint Behavior-->
    <behavior name="AADEnabledEndpointBehavior">
      <bearerTokenRequired/>
    </behavior>
  </endpointBehaviors>
</behaviors>
<extensions>
  <behaviorExtensions> <!--Linking the BearerTokenExtensionElement-->
    <add name="bearerTokenRequired" type="TokenValidator.BearerTokenExtensionElement, TokenValidator"/>
  </behaviorExtensions>
</extensions>

Your WCF service should now accept AAD Tokens on this custom AAD endpoint and your tenants will be able to consume the same by just changing the binding and endpoint from their side. Note that you will need to add the tenant's client ID in the allowedTenantIDs list in web.config so as to authorize the tenant to hit your service.


Final Note - Though I have implemented Microsoft's AAD Based Authentication, you should be able to reuse the whole code to implement any OAuth based Identity Provider's Token Validation. You just need to change the respective keys for AADAuthority in web.config.

halfer
  • 19,824
  • 17
  • 99
  • 186
Abhishek Anand
  • 101
  • 1
  • 8
0

You can implement Bearer token authentication

using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Http;

[assembly: OwinStartup(typeof(ns.Startup))]

namespace ns
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();

            ConfigureOAuth(app);

            WebApiConfig.Register(config);
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            app.UseWebApi(config);

            config.MessageHandlers.Add(new LogRequestAndResponseHandler());
        }

Configure using of OAuthBearerAuthentication:

        public void ConfigureOAuth(IAppBuilder app)
        {
            OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/TokenService"),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(3),
                Provider = new SimpleAuthorizationServerProvider()
            };

            // Token Generation
            app.UseOAuthAuthorizationServer(OAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        }

And finally set the identity claims

        public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
        {
            public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
            {
                context.Validated();
            }

            public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
            {
                context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

                try
                {
                    var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                    identity.AddClaim(new Claim(ClaimTypes.Name, "Name"));
                    identity.AddClaim(new Claim(ClaimTypes.Sid, "Sid"));
                    identity.AddClaim(new Claim(ClaimTypes.Role, "Role"));

                    context.Validated(identity);
                }
                catch (System.Exception ex)
                {
                    context.SetError("Error....");
                    context.Response.Headers.Add("X-Challenge", new[] { ((int)HttpStatusCode.InternalServerError).ToString() });
                }
            }
        }
    }
}

It's the easiest solution and works like a charm!

Cyrus
  • 2,261
  • 2
  • 22
  • 37