The One With

March 2011 - Posts

  • OData and OAuth - Part 5 – OAuth 1.0 Service Provider in ASP.NET MVC

    OAuthImplementing an OAuth service provider is not a difficult task once you select an OAuth library that you like. The one I am going to be using is the one I like the most. It’s simple to use and gives me a lot of freedom when it comes to token management.

    The authentication flow should already be familiar from our experience as an OAuth consumer

    Let’s follow the spec. From section 6 - Authenticating with OAuth of the 1.0 spec:

    Service Provider Grants Request Token

    Typically, you would write and IHttpHandler for handling OAuth requests in ASP.NET. I find that it’s much cleaner to have them go through the MVC Controller.

    public ActionResult GetRequestToken() {
        Response response 
            = ServiceProvider.GetRequestToken(
                Request.HttpMethod,
                Request.Url);
        
        this.Response.StatusCode = response.StatusCode;
        return Content(response.Content, response.ContentType);
    }

    Behind the scenes, ServiceProvider.GetRequestToken() will parse the query parameters and validate the signature of the request based on the specified signing method. The specified consumer key will be validated using a ConsumerStore object that we can configure in Web.config.

    <section name="oauth.consumerTokenStore" type="BankOfShire.OAuth.ConsumerTokenStore" />

    The default implementation accepts anonymous/anonymous but it’s a good idea to require a consumer registration and issue consumer keys and secrets. Just like we did for twitter here.

    public class ConsumerStore : ConfigurationSection, IConsumerStore {
        public virtual IConsumer GetConsumer(string consumerKey) {
            if (String.Equals(consumerKey, "anonymous")) {
                return new ConsumerBase() {
                    CallbackUri = null,
                    ConsumerKey = "anonymous",
                    ConsumerSecret = "anonymous",
                };
            }
            return null;
        }
    }

    The responsibility for issuing request tokens is assigned to RequestTokenStore. Configurable in Web.config:

    <section name="oauth.requestTokenStore" type="BankOfShire.OAuth.RequestTokenCache" />

    Request tokens in OAuth 1.0 are short lived. They should only be valid during the authentication process. If you look at the authentication flow above, that would be from B to F.

    So how long should the request token lifetime be? Yahoo for example issues tokens that expire after 1 hour. Is this enough time for the user to type in their credentials? I think so. Maybe even too long. Remember, longer lifespan means more physical hardware. The ideal situation is too keep request tokens in memory without having to persist them in the database. For small sites, ASP.NET Cache should do fine:

    public class RequestTokenCache : RequestTokenStore {
        public override IToken GetToken(string token) {
            HttpContext context = HttpContext.Current;
            if (context != null) {
                return context.Cache.Get("rt:" + token) as IToken;
            }
            return null;
        }
    
        public override IToken AuthorizeToken(string token, 
                string authenticationTicket) {
            HttpContext context = HttpContext.Current;
            if (context == null) {
                return null;
            }
            IToken unauthorizeToken = context.Cache.Get("rt:" + token) as IToken;
            if (unauthorizeToken == null
                            || unauthorizeToken.IsEmpty) {
                return null;
            }
            IToken authorizeToken = new Token(
                unauthorizeToken.ConsumerKey,
                unauthorizeToken.ConsumerKey,
                unauthorizeToken.Value,
                unauthorizeToken.Secret,
                authenticationTicket,
                unauthorizeToken.Verifier,
                unauthorizeToken.Callback);            
            context.Cache.Insert(
                "rt:" + authorizeToken.Value,
                authorizeToken,
                null,
                DateTime.UtcNow.AddSeconds(base.ExpirationSeconds),
                System.Web.Caching.Cache.NoSlidingExpiration
            );
            return authorizeToken;
        }
    
        public override IToken CreateUnauthorizeToken(string consumerKey, 
                string consumerSecret, string callback) {
            IToken unauthorizeToken = new Token(
                    consumerKey,
                    consumerSecret,
                    Token.NewToken(TokenLength.Long),
                    Token.NewToken(TokenLength.Long),
                    String.Empty, /* unauthorized */
                    Token.NewToken(TokenLength.Short),
                    callback);
            HttpContext context = HttpContext.Current;
            if (context != null) {
                context.Cache.Insert(
                       "rt:" + unauthorizeToken.Value,
                       unauthorizeToken,
                       null,
                       DateTime.UtcNow.AddSeconds(base.ExpirationSeconds),
                       System.Web.Caching.Cache.NoSlidingExpiration
                    );
            }
            return unauthorizeToken;
        }
    }

    But I strongly recommend exploring other caching solutions: Memcached, Velocity and other distributed caches if you can. If you can’t, just store them in the database and do some clean up periodically.

    Service Provider Directs User to Consumer

    Once the request token is issued, the consumer will redirect to the provider site so that the user can enter the credentials. We’ll need to markup some UI.

    [HttpPost]
    public ActionResult AuthorizeToken(AuthorizeModel model) {
        try {
            IToken token;
            if (!VerifyRequestToken(out token)) {
                return View(model);
            }
            if (model.Allow != "Allow") {
                return Redirect(token.Callback);
            }
            MixedAuthenticationIdentity identity 
                    = MixedAuthentication.GetMixedIdentity();
            if (identity == null
                            || !identity.IsAuthenticated) {
                if (!ModelState.IsValid) {
                    return View(model);
                }
            }
            MixedAuthenticationTicket ticket
                = identity != null && identity.IsAuthenticated ?
                    identity.CreateTicket() :
                    MembershipService.ResolvePrincipalIdentity(
                        model.Email,
                        model.Password);
            bool authorized = false;
            if (CanImpersonate(ticket)) {
                if (AuthorizeRequestToken(
                        ticket,
                        out token)) {
                    MixedAuthentication.SetAuthCookie(ticket, true, null);
                    authorized = true;
                }
            } else {
                ModelState.AddModelError("", "Email address or Password is incorrect.");
            }
            if (authorized) {
                Uri returnUri = ((Url)token.Callback).ToUri(
                    Parameter.Token(token.Value),
                    Parameter.Verifier(token.Verifier));
                return Redirect(returnUri.ToString());
            }
            return View(model);
        } catch (Exception e) {
            ModelState.AddModelError("", e);
            return View(model);
        }
    }

    ServiceProvider has a helper method to deal with request token authorization. We’ll authorize request tokens using the same ticket that we use for normal sign-ins. (You can read about MixedAuthenticationTicket here.)  The RequestTokenStore will be responsible for issuing authorized tokens. See RequestTokenStore.AuthorizeToken above.

    bool AuthorizeRequestToken(MixedAuthenticationTicket ticket, 
            out IToken token) {
        ValidationScope scope;
        token = ServiceProvider.AuthorizeRequestToken(
                    Request.HttpMethod,
                    Request.Url,
                    ticket.ToJson(),
                    out scope);
        if (token == null || token.IsEmpty) {
            if (scope != null) {
                foreach (ValidationError error in scope.Errors) {
                    ModelState.AddModelError("", error.Message);
                    return false;
                }
            }
            ModelState.AddModelError("", "Invalid / expired token");
            return false;
        }
        return true;
    }

    image

    Service Provider Grants Access Token

    Once the request token has been authorized, we need to issue a permanent (or long lived) access token.

    public ActionResult GetAccessToken() {
        Response response 
            = ServiceProvider.GetAccessToken(
                Request.HttpMethod,
                Request.Url);
    
        this.Response.StatusCode = response.StatusCode;            
        return Content(response.Content, response.ContentType);
    }

    It will be the responsibility of the AccessTokenStore to issue new tokens. Here is an implementation that we use for our site. Remember, we use XPO for our data access with MySql in the back.

    <section name="oauth.accessTokenStore" type="BankOfShire.OAuth.AccessTokenCache" />
    public class AccessTokenCache : AccessTokenStore {
        public override IToken GetToken(string token) {
            if (String.IsNullOrEmpty(token)) {
                return null;
            }
            IToken t = null;
            using (Session session = _Global.CreateSession()) {
                t = session.GetObjectByKey<Data.Token>(token);
                if (t == null
                                || t.IsEmpty) {
                    return null;
                }
            }
            return t;
        }
        public override void RevokeToken(string token) {
            if (String.IsNullOrEmpty(token)) {
                return;
            }
            using (Session session = _Global.CreateSession()) {
                session.BeginTransaction();
                Data.Token t = session.GetObjectByKey<Data.Token>(token);
                if (t != null) {
                    t.Delete();
                }
                session.CommitTransaction();
            }
        }
        public override IToken CreateToken(IToken requestToken) {
            if (requestToken == null || requestToken.IsEmpty) {
                throw new ArgumentException("requestToken is null or empty.");
            }
            Token token = new Token(
                requestToken.ConsumerKey,
                requestToken.ConsumerSecret,
                Token.NewToken(TokenLength.Long),
                Token.NewToken(TokenLength.Long),
                requestToken.AuthenticationTicket,
                String.Empty,
                String.Empty
            );
            using (Session session = _Global.CreateSession()) {
                DateTime utcNow = DateTime.UtcNow;
                session.BeginTransaction();
                Data.Token t = new Data.Token(session);
                t.Value = token.Value;
                t.Secret = token.Secret;
                t.ConsumerKey = token.ConsumerKey;
                t.ConsumerSecret = token.ConsumerSecret;
                t.AuthenticationTicket = token.AuthenticationTicket;
                t.Created = utcNow;
                t.Modified = utcNow;
                t.Save();          
                session.CommitTransaction();
            }
            return token;
        }
    }

    An XPO object for our token store:

    [Persistent("Tokens")]
    public class Token : XPLiteObject, DevExpress.OAuth.IToken {
        public Token() : base() { }
        public Token(Session session) : base(session) { }
        
        [Key(AutoGenerate = false)]
        [Size(128)]
        public string Value { get; set; }
        [Size(128)]
        public string Secret { get; set; }
    
        [Indexed(Unique=false)]
        [Size(128)]
        public string ConsumerKey { get; set; }
        [Size(128)]
        public string ConsumerSecret { get; set; }
    
        [Size(SizeAttribute.Unlimited)]
        public string AuthenticationTicket { get; set; }
    
        public DateTime Created { get; set; }
        public DateTime Modified { get; set; }
    
        bool DevExpress.OAuth.IToken.IsEmpty {
            get {
                return String.IsNullOrEmpty(Value) ||
                    String.IsNullOrEmpty(Secret) ||
                    String.IsNullOrEmpty(ConsumerKey) ||
                    String.IsNullOrEmpty(ConsumerSecret);
            }
        }
    
        bool DevExpress.OAuth.IToken.IsCallbackConfirmed {
            get { 
                return true; 
            }
        }
    
        string DevExpress.OAuth.IToken.Callback {
            get { return String.Empty; }
        }
    
        string DevExpress.OAuth.IToken.Verifier {
            get { return String.Empty; }
        }
    }

    That’s it, as far as the authentication flow is goes. Now when a request for a protected resources comes in (with a previously issued access token) we can check the Authorization’ header and create the appropriate MixedAuthenticationIdentity.

    MixedAuthenticationTicket ticket;
    stringauthorizationHeader = HttpContext.Current != null?
            HttpContext.Current.Request.Headers["Authorization"] : null;
    if(!String.IsNullOrEmpty(authorizationHeader)) {
       
        ValidationScope scope;               
        TokenIdentity tokenIdentity = ServiceProvider.GetTokenIdentity(
                        context.Request.HttpMethod,
                        context.Request.Url,
                        authorizationHeader,
                        outscope);
        if(tokenIdentity == null) {
            if(scope != null) {
                foreach (ValidationError error inscope.Errors) {
                    throw newHttpException(error.StatusCode, error.Message);
                }
            }
            return null;
        }
        ticket = MixedAuthenticationTicket.FromJson(tokenIdentity.AuthenticationTicket);

    }

    Note that the MixedAuthentication helper class takes care of this so our Application_AuthenticateRequest will work the same for both Forms Authentication (cookie) and OAuth (Access Token).

    protected void Application_AuthenticateRequest(object sender, EventArgs e) {
        MixedAuthenticationPrincipal p = MixedAuthentication.GetMixedPrincipal();
    
        if (p != null) {
            HttpContext.Current.User = p;
            Thread.CurrentPrincipal = p;
        }
    }

     

    What’s next

    Download source code for Part 5

    Cheers

    Azret

LIVE CHAT

Chat is one of the many ways you can contact members of the DevExpress Team.
We are available Monday-Friday between 7:30am and 4:30pm Pacific Time.

If you need additional product information, write to us at info@devexpress.com or call us at +1 (818) 844-3383

FOLLOW US

DevExpress engineers feature-complete Presentation Controls, IDE Productivity Tools, Business Application Frameworks, and Reporting Systems for Visual Studio, along with high-performance HTML JS Mobile Frameworks for developers targeting iOS, Android and Windows Phone. Whether using WPF, ASP.NET, WinForms, HTML5 or Windows 10, DevExpress tools help you build and deliver your best in the shortest time possible.

Copyright © 1998-2017 Developer Express Inc.
All trademarks or registered trademarks are property of their respective owners