Claim-based-security for ASP.NET Web APIs using DotNetOpenAuth

Recently I worked with a customer assisting them in implementing their Web APIs using the new ASP.NET Web API framework. Their API would be public so obviously security came up as the key concern to address. Claims-Based-Security is widely used in SOAP/WS-* world and we have rich APIs available in .NET Framework in the form of WCF, WIF & ADFS 2.0. Even though we now have this cool library to develop Web APIs, the claims-based-security story for REST/HTTP is still catching up. OAuth 2.0 is almost ready, OpenID Connect is catching up quickly however it would still take sometime before we have WIF equivalent libraries for implementing claims-based-security in REST/HTTP world. DotNetOpenAuth seems to be the most prominent open-source library claiming to support OAuth 2.0 so I decided to give it a go to implement the ‘Resource Owner Password Credentials’authorization grant. Following diagram shows the solution structure for my target scenario.

1. OAuth 2.0 issuer is an ASP.NET MVC application responsible for issuing token based on OAuth 2.0 ‘Password Credentials’ grant type.

2. Web API Host exposes secured Web APIs which can only be accessed by presenting a valid token issued by the trusted issuer

3. Sample thick client which consumes the Web API

I have used the DotNetOpenAuth.Ultimate NuGet package which is just a single assembly implementing quite a few security protocols. From OAuth 2.0 perspective, AuthorizationServer is the main class responsible for processing the token issuance request, producing and returning a token for valid & authenticated request. The token issuance action of my OAuthIssuerController looks like this:

OAuth 2.0 Issuer

public class OAuthIssuerController : Controller {
    public ActionResult Index()
    {
        var configuration = new IssuerConfiguration {
            EncryptionCertificate = new X509Certificate2(Server.MapPath("~/Certs/localhost.cer")),
            SigningCertificate = new X509Certificate2(Server.MapPath("~/Certs/localhost.pfx"), "a")
        };

        var authorizationServer = new AuthorizationServer(new OAuth2Issuer(configuration));
        var response = authorizationServer.HandleTokenRequest(Request).AsActionResult();

        return response;
    }
}

AuthorizationServer handles all the protocol details and delegate the real token issuance logic to a custom token issuer handler (OAuth2Issuer in following snippet)

Protocol independent issuer

public class OAuth2Issuer : IAuthorizationServer
{
    private readonly IssuerConfiguration _configuration;
    public OAuth2Issuer(IssuerConfiguration configuration)
    {
        if (configuration == null) throw new ArgumentNullException(“configuration”);
        _configuration = configuration;
    }
    public RSACryptoServiceProvider AccessTokenSigningKey
    {
        get
        {
            return (RSACryptoServiceProvider)_configuration.SigningCertificate.PrivateKey;
        }
    }
    public DotNetOpenAuth.Messaging.Bindings.ICryptoKeyStore CryptoKeyStore
    {
        get { throw new NotImplementedException(); }
    }
    public TimeSpan GetAccessTokenLifetime(DotNetOpenAuth.OAuth2.Messages.IAccessTokenRequest accessTokenRequestMessage)
    {
        return _configuration.TokenLifetime;
    }
    public IClientDescription GetClient(string clientIdentifier)
    {
        const string secretPassword = “test1243″;
        return new ClientDescription(secretPassword, new Uri(“http://localhost/”), ClientType.Confidential);
    }
    public RSACryptoServiceProvider GetResourceServerEncryptionKey(DotNetOpenAuth.OAuth2.Messages.IAccessTokenRequest accessTokenRequestMessage)
    {
        return (RSACryptoServiceProvider)_configuration.EncryptionCertificate.PublicKey.Key;
    }
    public bool IsAuthorizationValid(DotNetOpenAuth.OAuth2.ChannelElements.IAuthorizationDescription authorization)
    {
        //claims added to the token
        authorization.Scope.Add(“adminstrator”);
        authorization.Scope.Add(“poweruser”);
        return true;
    }
    public bool IsResourceOwnerCredentialValid(string userName, string password)
    {
        return true;
    }
    public DotNetOpenAuth.Messaging.Bindings.INonceStore VerificationCodeNonceStore
    {
        get
        {
            throw new NotImplementedException();
        }
    }
}

Now with my issuer setup, I can acquire access tokens by POSTing following request to the token issuer endpoint

Client

POST /Issuer HTTP/1.1

Content-Type: application/x-www-form-urlencoded; charset=utf-8

scope=http%3A%2F%2Flocalhost%2F&grant_type=client_credentials&client_id=zamd&client_secret=test1243

In response, I get 200 OK with following payload

HTTP/1.1 200 OK

Cache-Control: no-cache, no-store, max-age=0, must-revalidate

Pragma: no-cache

Content-Type: application/json; charset=utf-8

Server: Microsoft-IIS/7.5

Content-Length: 685

{“access_token”:”gAAAAC5KksmbH0FyG5snks_xOcROnIcPldpgksi5b8Egk7DmrRhbswiEYCX7RLdb2l0siW8ZWyqTqxOFxBCjthjTfAHrE8owe3hPxur7Wmn2LZciTYfTlKQZW6ujlhEv6N4V1HL4Md5hdtwy51_7RMzGG6MvvNbEU8_3GauIgaF7JcbQJAEAAIAAAABR4tbwLFF57frAdPyZsIeA6ljo_Y01u-2p5KTfJ2xa6ZhtEpzmC46Omcvps9MbFWgyz6536_77jx9nE3sePTSeyB5zyLznkGDKhjfWwx3KjbYnxCVCV-n2pqKtry0l8nkMj4MrjqoTXpvd_P0c_VGfVXCsVt7BYOO68QbD-m7Yz9rHIZn-CQ4po0FqS2elDVe9qwu_uATbAmOXlkWsbnFwa6_ZDHcSr2M-WZxHTVFin7vEWO7FxIQStabu_r4_0Mo_xaFlBKp2hl9Podq8ltx7KvhqFS0Xu8oIJGp1t5lQKoaJSRTgU8N8iEyQfCeU5hvynZVeoVPaXfMA-gyYfMGspLybaw7XaBOuFJ20-BZW0sAFGm_0sqNq7CLm7LibWNw”,”token_type”:”bearer”,”expires_in”:”300″,”scope”:”http:\/\/localhost\/ adminstrator poweruser”}

DotNetOpenAuth also has a WebServerClient class which can be used to acquire tokens and I have used in my test application instead of crafting raw HTTP requests. Following code snippet generates the same above request/response

Get Access Token

private static IAuthorizationState GetAccessToken()
{
    var authorizationServer = new AuthorizationServerDescription
    {
        TokenEndpoint = new Uri(“http://localhost:1960/Issuer”),
        ProtocolVersion = ProtocolVersion.V20
    };
    var client = new WebServerClient(authorizationServer, “http://localhost/”);
    client.ClientIdentifier = “zamd”;
    client.ClientSecret = “test1243″;
    var state = client.GetClientAccessToken(new[] { “http://localhost/” });
    return state;
}

Ok Now the 2nd part is to use this access token for authentication & authorization when consuming ASP.NET Web APIs.

Web API Client

static void Main(string[] args)
{
    var state = GetAccessToken();
    Console.WriteLine(“Expires = {0}”, state.AccessTokenExpirationUtc);
    Console.WriteLine(“Token = {0}”, state.AccessToken);
    var httpClient = new OAuthHttpClient(state.AccessToken)
    {
        BaseAddress = new Uri(“http://localhost:2150/api/values”)
    };
    Console.WriteLine(“Calling web api…”);
    Console.WriteLine();
    var response = httpClient.GetAsync(“”).Result;
    if (response.StatusCode==HttpStatusCode.OK)
        Console.WriteLine(response.Content.ReadAsStringAsync().Result);
    else
        Console.WriteLine(response);
    Console.ReadLine();
}

On line 8, I’m creating an instance of a customized HttpClient passing in the access token. The httpClient would use this access token for all subsequent HTTP requests

OAuth enabled HttpClient

public class OAuthHttpClient : HttpClient
{
    public OAuthHttpClient(string accessToken)
        : base(new OAuthTokenHandler(accessToken))
    {
    }
    class OAuthTokenHandler : MessageProcessingHandler
    {
        string _accessToken;
        public OAuthTokenHandler(string accessToken)
            : base(new HttpClientHandler())
        {
            _accessToken = accessToken;
        }
        protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue(“Bearer”, _accessToken);
            return request;
        }
        protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, System.Threading.CancellationToken cancellationToken)
        {
            return response;
        }
    }
}

Relying Party (ASP.NET Web APIs)

Finally on the RP side, I have used standard MessageHandler extensibility to extract and validate the ‘access token’. The OAuth2 message handler also extracts the claims from the access token and create a ClaimsPrincipal which is passed on the Web API implementation for authorization decisions.

OAuth2 Message Handler

public class OAuth2Handler : DelegatingHandler
{
    private readonly ResourceServerConfiguration _configuration;
    public OAuth2Handler(ResourceServerConfiguration configuration)
    {
        if (configuration == null) throw new ArgumentNullException(“configuration”);
        _configuration = configuration;
    }
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpContextBase httpContext;
        string userName;
        HashSet<string> scope;
        if (!request.TryGetHttpContext(out httpContext))
            throw new InvalidOperationException(“HttpContext must not be null.”);
        var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(
                                                    (RSACryptoServiceProvider)_configuration.IssuerSigningCertificate.PublicKey.Key,
                                                    (RSACryptoServiceProvider)_configuration.EncryptionVerificationCertificate.PrivateKey));
        var error = resourceServer.VerifyAccess(httpContext.Request, out userName, out scope);
        if (error != null)
            return Task<HttpResponseMessage>.Factory.StartNew(error.ToHttpResponseMessage);
        var identity = new ClaimsIdentity(scope.Select(s => new Claim(s, s)));
        if (!string.IsNullOrEmpty(userName))
            identity.Claims.Add(new Claim(ClaimTypes.Name, userName));
        httpContext.User = ClaimsPrincipal.CreateFromIdentity(identity);
        Thread.CurrentPrincipal = httpContext.User;
        return base.SendAsync(request, cancellationToken);
    }
}

Inside my Web API, I access the claims information using the standard IClaimsIdentity abstraction.

Accessing claims information

public IEnumerable<string> Get()
{
    if (User.Identity.IsAuthenticated && User.Identity is IClaimsIdentity)
        return ((IClaimsIdentity) User.Identity).Claims.Select(c => c.Value);
    return new string[] { “value1″, “value2″ };
}

Fiddler Testing

Once I got the “access token”, I can test few scenarios in fiddler by attaching and tweaking the token when calling my web api.

401 without an “access token”

200 OK with a Valid token

401 with Expired token

401 with Tempered token

Source code attached. Please feel free to download and use.

Original Post by ZulfiqarAhmed on May4th, 2012

Here: http://zamd.net/2012/05/04/claim-based-security-for-asp-net-web-apis-using-dotnetopenauth/

时间: 2024-10-05 05:55:34

Claim-based-security for ASP.NET Web APIs using DotNetOpenAuth的相关文章

基于.Net Framework 4.0 Web API开发(4):ASP.NET Web APIs 基于令牌TOKEN验证的实现

概述:  ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作.但是在使用API的时候总会遇到跨域请求的问题, 特别各种APP万花齐放的今天,对API使用者身份角色验证是不能避免的(完全开发的API不需要对使用者身份角色进行管控,可以绕过),这篇文章就来谈谈基于令牌TOKEN身份验证的实现. 问题: 对于Web API的选择性的开放,使用者无论使用AJAX,还是HttpClient对接,总要对使用者的身份角色

基于.Net Framework 4.0 Web API开发(5):ASP.NET Web APIs AJAX 跨域请求解决办法(CORS实现)

概述:  ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作.但是在使用API的时候总会遇到跨域请求的问题,特别各种APP万花齐放的今天,API的跨域请求是不能避免的. 在默认情况下,为了防止CSRF跨站的伪造攻击(或者是 javascript的同源策略(Same-Origin Policy)),一个网页从另外一个域获取数据时就会收到限制.有一些方法可以突破这个限制,那就是大家熟知的JSONP, 当然这只是

基于.Net Framework 4.0 Web API开发(3):ASP.NET Web APIs 异常的统一处理Attribute 和统一写Log 的Attribute的实现

概述:  ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作.但是项目,总有异常发生,本节就来谈谈API的异常的统一处理和写统一写log逻辑的解决方案. 问题: 在ASP.NET Web API编写时,如果每个API都写异常处理逻辑,不但加大了开发工作量,且每个开发人员处理异常返回的数据结构也不尽相同,在异常发生情况下,客户端处理异常的逻辑就不再通用,也同时加大了对接接口人员的工作量,好的API错误码和错误

基于.Net Framework 4.0 Web API开发(2):ASP.NET Web APIs 参数传递方式详解

概述:  ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作.调用API过程中参数的传递是必须的,本节就来谈谈API使用过程中参数的传递方式. 各种参数传递方式的实现: ASP.NET Web API参数有两种传递方式,一种是请求时携带QueryString,对应API 开发中的FromUrlAttribute属性,也是参数传递默认属性,并且每个API可以含有多个此类型的参数,主要应对GET请求,但此种方式

在ASP.NET Web API 2中使用Owin OAuth 刷新令牌

在上篇文章介绍了Web Api中使用令牌进行授权的后端实现方法,基于WebApi2和OWIN OAuth实现了获取access token,使用token访问需授权的资源信息.本文将介绍在Web Api中启用刷新令牌的后端实现. 本文要用到上篇文章所使用的代码,代码编写环境为VS 2017..Net Framework 4.7.2,数据库为MS SQL 2008 R2. OAuth 刷新令牌 上文已经搞了一套Token授权访问,这里有多出来一个刷新令牌(Refresh Token),平白添加程序

Implement JSON Web Tokens Authentication in ASP.NET Web API and Identity 2.1

http://bitoftech.net/2015/02/16/implement-oauth-json-web-tokens-authentication-in-asp-net-web-api-and-identity-2/ Currently our API doesn’t support authentication and authorization, all the requests we receive to any end point are done anonymously, I

JSON Web Token in ASP.NET Web API 2 using Owin

In the previous post Decouple OWIN Authorization Server from Resource Server we saw how we can separate the Authorization Server and the Resource Server by unifying the "decryptionKey" and "validationKey" key values in machineKey node

ASP.NET Web API 2基于令牌的身份验证

基于令牌的认证 我们知道WEB网站的身份验证一般通过session或者cookie完成的,登录成功后客户端发送的任何请求都带上cookie,服务端根据客户端发送来的cookie来识别用户. WEB API使用这样的方法不是很适合,于是就有了基于令牌的认证,使用令牌认证有几个好处:可扩展性.松散耦合.移动终端调用比较简单等等,别人都用上了,你还有理由不用吗? 下面我们花个20分钟的时间来实现一个简单的WEB API token认证: Step 1: 新建一个空的WEB API项目,项目名称就设置为

【ASP.NET Web API教程】2.4 创建Web API的帮助页面

参考页面: http://www.yuanjiaocheng.net/CSharp/csharprumenshili.html http://www.yuanjiaocheng.net/entity/mode-first.html http://www.yuanjiaocheng.net/entity/database-first.html http://www.yuanjiaocheng.net/entity/choose-development-approach.html http://ww