Blogs

The One With

OData and OAuth - Part 2 – FormsAuthenticationTicket & MixedAuthentication

     

Continuing where we left off last time, let’s take a closer look at how we do authentication. When a member signs in by clicking on his name

we look up the Principal object and if it exists we set a cookie with the FormsAuthenticationTicket

public ActionResult Signin(Nullable<Guid> id) {
    if (id.HasValue && id != Guid.Empty) {
        Principal p = MembershipService.GetPrincipal(id.Value);
        if (p != null) { 
            FormsAuthentication.SetAuthCookie(p.Email, true);
            return RedirectToAction("", "home");
        }
    } 
    return View(new Signin(MembershipService.GetPrincipals()));
}

we do this using forms authentication facilities in ASP.NET - we configured it in Web.Config  <authenticationmode="Forms" />

For simplicity of course, we’re not using any passwords. This is The Shire after all, there is no way, for example, Gandolf would sign in as Sam Smile.

Once the cookie set, forms authentication will create an IIdentity from which we can access the encrypted FormsAuthenticationTicket.

and if you look closer, the only information about the user that we have is the user name IIdentity.Name. For the most part this is enough to query all the relevant information from the database. But I don’t really want to hit the DB on every single request for bits of information that does not change. Let’s just store it in the cookie.

A common practice is to store additional bits in the FormsAuthenticationTicket.UserData. This way, what ever we store in there (First and Last names, ID, Roles etc…) will be encrypted. Our new Signin action:

public ActionResult Signin(Nullable<Guid> id) {
    if (id.HasValue && id != Guid.Empty) {        
        MixedAuthenticationTicket ticket = new MixedAuthenticationTicket(
                    "Principal",
                    id.Value.ToString("N"),
                    String.Empty /* Email [Optional] */,
                    id  /* Principal ID */,
                    string.Empty /* Full Name [Optional] */);
        if (Impersonate(ticket, true)) {            
            return RedirectToAction("", "home");
        }
    }
    return View(new Signin(MembershipService.GetPrincipals()));
}
bool Impersonate(
    MixedAuthenticationTicket ticket,
    bool remember) {
    bool bOK = false;
    if (ticket.IsAuthenticated 
            && ticket.Principal.HasValue 
            && ticket.Principal.Value != Guid.Empty) {

        Principal p = MembershipService.GetPrincipal(ticket.Principal.Value);
        if (p != null) {
            ticket.FullName = p.FullName;
            ticket.Email = p.Email;

            MixedAuthentication.SetAuthCookie(ticket, remember, null);
            bOK = true;
        }
    }
    return bOK;
}

Now let’s make sure we read the values back from the cookie and have them available through out the lifetime of the request:   

public class Global : System.Web.HttpApplication {
    protected void Application_AuthenticateRequest(object sender, EventArgs e) {
        MixedAuthenticationPrincipal p = HttpContext.Current.User.GetMixedPrincipal();
        if (p != null) {
            HttpContext.Current.User = p;
            Thread.CurrentPrincipal = p;
        }
    }
}

Our IIdentity is now:

Let’s use the full name in the top menu:

Before:

if ((HttpContext.Current.User != null)  && (HttpContext.Current.User.Identity != null)
               && HttpContext.Current.User.Identity.IsAuthenticated) {
       <div>           
           @Html.ActionLink(
               HttpContext.Current.User.Identity.Name,
               "", "home",
               null,
               null)
       </div>                                                      
}

After:

if ((HttpContext.Current.User != null)  && (HttpContext.Current.User.Identity != null)
                && HttpContext.Current.User.Identity.IsAuthenticated) {

        DevExpress.OAuth.Web.Authentication.MixedAuthenticationIdentity identity
            = HttpContext.Current.User.Identity as DevExpress.OAuth.Web.Authentication.MixedAuthenticationIdentity;
        <div>            
            @Html.ActionLink(
                identity != null && !string.IsNullOrEmpty(identity.FullName) ? identity.FullName :
                             HttpContext.Current.User.Identity.Name,
                "", "home",
                null,
                null)
        </div>                                                        
}

The API

MixedIdentity is sorted so let’s go back to our OData service and make sure we only expose things that a member is authorized to see. So if previously, a call to get Principals would list everyone, we now want to make sure that only the Principal for the authenticated user is listed. Same for Valuables.

[ConnectionString("BankOfShire")]
public class OData : XpoDataService {
    public static void InitializeService(DataServiceConfiguration config) {
        config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
        config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
    }

    protected override void RegisterEntities() {
        RegisterEntity(typeof(Principal), "Principal", null, new String[] {} );
        RegisterEntity(typeof(Valuable), "Valuables", null, new String[] {} );
    }

    [QueryInterceptor("Principal")]
    public Expression<Func<Principal, bool>> AuthorizedPrincipals() {
        if (!HttpContext.Current.Request.IsAuthenticated) {
            return (principal) => false;
        }
        MixedAuthenticationIdentity identity = HttpContext.Current.User.Identity as MixedAuthenticationIdentity;
        if (identity == null
                            || !identity.Principal.HasValue) {
            return (principal) => false;
        }
        return (principal) => principal.ID == identity.Principal.Value;
    }

    [QueryInterceptor("Valuables")]
    public Expression<Func<Valuable, bool>> AuthorizedValuable() {
        if (!HttpContext.Current.Request.IsAuthenticated) {
            return (valuable) => false;
        }
        MixedAuthenticationIdentity identity = HttpContext.Current.User.Identity as MixedAuthenticationIdentity;
        if (identity == null
                            || !identity.Principal.HasValue) {
            return (valuable) => false;
        }
        return (valuable) => valuable.Owner == identity.Principal.Value;
    }
}

Now if we are to sign in and make a request (from the same browser)

http://localhost:65099/data/api/odata.svc/principal

we will only see data that belongs to the signed-in user:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?> 
 <feed xml:base="http://localhost:65099/data/api/OData.svc/" 
            xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" 
            xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" 
            xmlns="http://www.w3.org/2005/Atom">
  <title type="text">principal</title> 
  <id>http://localhost:65099/data/api/odata.svc/principal</id> 
  <updated>2011-02-25T02:13:58Z</updated> 
  <link rel="self" title="principal" href="principal" /> 
 <entry>
  <id>http://localhost:65099/data/api/OData.svc/Principal(guid'4306ffae-a80e-419d-823d-5a7cd2986bfa')</id> 
  <title type="text" /> 
  <updated>2011-02-25T02:13:58Z</updated> 
 <author>
  <name /> 
  </author>
  <link rel="edit" title="Principal" 
    href="Principal(guid'4306ffae-a80e-419d-823d-5a7cd2986bfa')" /> 
  <category term="BankOfShire.Data.Principal" 
        scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" /> 
 <content type="application/xml">
 <m:properties>
  <d:ID m:type="Edm.Guid">4306ffae-a80e-419d-823d-5a7cd2986bfa</d:ID> 
  <d:FullName>Frodo</d:FullName> 
  <d:Picture m:type="Edm.Binary">iVBORo4mhUxtrHo31PdvRcaV2xEN4yc62DjZedjYvlak...</d:Picture>
  <d:Email>Frodo@TheShire.com</d:Email> 
  <d:Created m:type="Edm.DateTime">2011-02-25T01:52:08</d:Created> 
  <d:Modified m:type="Edm.DateTime">2011-02-25T01:52:08</d:Modified> 
  </m:properties>
  </content>
  </entry>
</feed>

SQL statement that XPO sent to MySql:

SELECT N0.`ID`,N0.`FULLNAME`,N0.`PICTURE`,N0.`EMAIL`,N0.`CREATED`,N0.`MODIFIED` 
FROM `PRINCIPALS` N0
WHERE (N0.`ID` = ?P0) ' WITH PARAMETERS {4306FFAE-A80E-419D-823D-5A7CD2986BFA}

Tips & Tricks

If you look at the Principal object, you’ll see that we store member picture as row bytes. This is not really useful, for the consumers of our API. The XPO Data Service Provider let’s you handle this situation using special Hidden and Visible attributes:

[Persistent("Principals")]
public class Principal : XPLiteObject {
    [Key(AutoGenerate = true)]
    public Guid ID { get; set; }
    [NonPersistent]
    [Visible]
    public string PictureUri {
        get {
            WebOperationContext ctx = WebOperationContext.Current;
            if (ctx != null) {
                Uri baseUri = ctx.IncomingRequest.UriTemplateMatch.BaseUri;
                return String.Format("{0}://{1}:{2}/account/picture?id={3}",
                    baseUri.Scheme,
                    baseUri.Host,
                    baseUri.Port,
                    ID.ToString("D"));
            } 
            return String.Empty; 
        }
        set { /* NOP */ }
    }
}

What’s Next

Download source code for Part 2

Cheers

Azret

Published Feb 24 2011, 07:11 PM by Azret Botash (DevExpress)
Filed under: , , , ,
Technorati tags: OAuth, MVC, MySQL, XPO, OData
Bookmark and Share

Comments

No Comments
More from DevExpress
Live Chat
Have a pre-sales question?
Need assistance with your evaluation?
We are here to help.
Chat is one of the many ways you can contact members of the DevExpress Team. We are available Monday-Friday between 8:30am and 5:00pm Pacific Time.
If you need additional product information, require pre-sales assistance, or want help with your order, write to us at info@devexpress.com or call us at
+1 (818) 844-3383.