The One With

February 2011 - Posts

  • OData and OAuth - Part 4 – Managing Identities

    We’re almost ready to implement an OAuth 1.0 Service provider. But first, let’s make sure our member accounts are properly password protected. Since we allow to sign in using different credentials into the same account, we need a place to store all those different identities.

    public enum AuthenticationType : int {
        Principal = 0,
        Google = 1,
        Facebook = 2,
        Twitter = 3,
    }
    
    [Persistent("Identities")]
    public class Identity : XPLiteObject {
        public Identity() : base() { }
        public Identity(Session session) : base(session) { }
    
        [Key(AutoGenerate = true)]
        public Guid ID { get; set; }
        
        [Indexed(Unique = true)]
        [Size(128)]
        public string Hash { get; set; }
    
        [Indexed(Unique = false)]
        public Nullable<Guid> Principal { get; set; }
    
        [Size(128)]
        public string Token { get; set; }
        [Size(128)]
        public string Secret { get; set; }
    
        public AuthenticationType AuthenticationType { get; set; }
    
        public DateTime Created { get; set; }
        public DateTime Modified { get; set; }
    }

    AuthenticationType

    Specified the type of the identity.

    Hash

    A unique value given to an identity by the provider. We must make sure that this value is unique per provider/authentication type.

    • AuthenticationType.Principal: We use “Principal” + Email address
    • AuthenticationType.Google: We use “Google” + Email address. Google does not let users change their email address so this value is unique.
    • AuthenticationType.Facebook: We use “Facebook” + ID. Facebook does allow users to change their email address. If we were to hash “Facebook” + Email address, and the user changes the email at a later time, we will not be able to resolve the identity after the change. So ID is what we need.
    • AuthenticationType.Twitter: We use “Twitter” + ID. Twitter doesn’t even provide email addresses but provides a screen name and an ID. Screen names can be changed so we can’t use them either. So ID is what we need.

    Note: Examine the FromTwitter, FromGoogle and FromFacebook methods in the AuthenticationService.cs

    To compute the hash value we use MixedAuthenticationTicket.GetHashCode:

    public string GetHashCode(string identitySecret) {    
        using (HMACSHA1 hash
                    = new HMACSHA1(Encoding.ASCII.GetBytes(identitySecret))) {
            
            return Convert.ToBase64String(
                hash.ComputeHash(Encoding.ASCII.GetBytes(
                    this.AuthenticationType.ToLowerInvariant() +
                    this.Identity.ToLowerInvariant())));
        }
    
    }
    
    

    The “identitySecret” - a hash key, we’ll configure in Web.config.

        <appSettings>
           <add key="Identity_Private_Key" value="tR3+Ty81lMeYAr/Fid0kMTYa/WM=" />
        </appSettings>
    

    Token & Secret

    This is where we store the latest access token and token secret that were issued by the provider. And in the case of AuthenticationType.Principal, we store the hashed password and the password hash key (salt).

    Principal

    An ID of the Principal object to resolve to.

    Putting it Together

    The new sign up page:

    [HttpPost]
    public ActionResult Signup(Signup model, string returnUrl) {
         if (!ModelState.IsValid) {
             return View(model);
         }
         try {
             MembershipCreateStatus createStatus;         
             MixedAuthenticationTicket ticket = MembershipService.CreatePrincipal(
                 model.Email, /* We use email as our primary identity */
                 model.Email,
                 model.FullName,
                 model.Password,
                 out createStatus);
    
             if (createStatus == MembershipCreateStatus.Success) {
                 MixedAuthentication.SetAuthCookie(ticket, true, null);
                 return RedirectToAction("", "home");
             }
    
             ModelState.AddModelError("", MembershipService.ErrorCodeToString(createStatus));
             return View(model);
         } catch (Exception e) {
             ModelState.AddModelError("", e);
             return View(model);
         }
    }
    
    

    Updated sign in page:

    [HttpPost]
    public ActionResult Signin(Signin model, string returnUrl) {
        if (!ModelState.IsValid) {
            return View(model);
        }
        try {                
            if (Impersonate(MembershipService.ResolvePrincipalIdentity(
                    model.Email, 
                    model.Password), true)) {
    
                return RedirectToAction("", "home");
            }
    
            ModelState.AddModelError("", "Email address or Password is incorrect.");
            return View(model);
        } catch (Exception e) {
            ModelState.AddModelError("", e);
            return View(model);
        }
    }
    
    public MixedAuthenticationTicket ResolvePrincipalIdentity(string identity, string password) {            
        MixedAuthenticationTicket ticket 
            = new MixedAuthenticationTicket("Principal", identity.ToLowerInvariant(), 
                String.Empty, 
                Guid.Empty,
                String.Empty);
        
        using (Session session = _Global.CreateSession()) {
            string identityCode 
                = ticket.GetHashCode(ConfigurationManager.AppSettings["Identity_Private_Key"]);
    
            Identity identityRecord = session.FindObject<Identity>(
                new BinaryOperator(
                        "Hash",
                        identityCode)
                );
            
            if (identityRecord == null) {
                return MixedAuthenticationTicket.Empty;
            }
    
            string encodedPassword = HMACSHA1(password, identityRecord.Secret);
            if (!encodedPassword.Equals(identityRecord.Token)) {
                return MixedAuthenticationTicket.Empty;
            }
    
            ticket.Principal = identityRecord.Principal;
            return ticket;
    
        }
    }
    

    For our own (Principal) authentication everything is straightforward and simple. For federated authentication it’s tricky. What should we do when someone signs in for example using Facebook for the first time? Should we auto create the identity and principal? Should we auto create the identity, redirect to the “confirm account” page and only than create the principal? Or should we keep the credentials in the cookie and only create persistent records when the user confirms the account?. My preference is toward option #2 for the following reasons:

    • Storing tokens (or any sensitive information) on the client side is not a good practice (even if we encrypt ticket cookie). And we need to keep track of the issued tokens so we have performs actions on user’s behalf.
    • The sign-in identity is unique (and will remain that way) and we’re going to persist it at some point anyway. So why not create it right away? (right after we acquired the token). You can put an expiration timestamp on auto created identities and GC them periodically.
    • Auto-creating principals without user’s confirmation is also not an option. What if one has an account already and wants to associate this *new* login with that *existing* account.

    in other words:

    public MixedAuthenticationTicket ResolveFederatedIdentity(
        MixedAuthenticationTicket ticket, IToken accessToken) {
    
        if (!ticket.IsAuthenticated) { return MixedAuthenticationTicket.Empty; }
       
        using (Session session = _Global.CreateSession()) {
            session.BeginTransaction();
            DateTime utcNow = DateTime.UtcNow;
            string identityCode 
                = ticket.GetHashCode(ConfigurationManager.AppSettings["Identity_Private_Key"]);
            Identity identityRecord = session.FindObject<Identity>(
                new BinaryOperator(
                        "Hash",
                        identityCode)
                );
            if (identityRecord != null) {
                if (identityRecord.Token == accessToken.Value
                                && identityRecord.Secret == accessToken.Secret) {
                    ticket.Principal = identityRecord.Principal;
                    return ticket;
                }
            }
            if (identityRecord == null) {
                identityRecord = new Identity(session);
                identityRecord.Created = utcNow;
                identityRecord.Hash = identityCode;
            }        
            identityRecord.Modified = utcNow;
            identityRecord.Token = accessToken.Value;
            identityRecord.Secret = accessToken.Secret;
    
            switch (ticket.AuthenticationType) {
                case "Google":
                    identityRecord.AuthenticationType = AuthenticationType.Google;
                    break;
                case "Facebook":
                    identityRecord.AuthenticationType = AuthenticationType.Facebook;
                    break;
                case "Twitter":
                    identityRecord.AuthenticationType = AuthenticationType.Twitter;
                    break;
                default:
                    throw new ArgumentException("Authentication type is not supported.", "ticket");
            }
            identityRecord.Save();
            session.CommitTransaction();
            ticket.Principal = identityRecord.Principal;
            return ticket;
        }
    }
    

    Now we need to confirm the account and create the principal:

    public MixedAuthenticationTicket ActivatePrincipal(
        MixedAuthenticationTicket ticket,
        out MembershipCreateStatus status) {
    
        status = MembershipCreateStatus.ProviderError;
        using (Session session = _Global.CreateSession()) {
            session.BeginTransaction();
            string identityCode 
                = ticket.GetHashCode(ConfigurationManager.AppSettings["Identity_Private_Key"]);        
            Identity identityRecord = session.FindObject<Identity>(
                new BinaryOperator(
                        "Hash",
                        identityCode)
                );
            if (identityRecord == null) {
                return MixedAuthenticationTicket.Empty;
            }
    
            /* Update or Create Principal */
            Principal principalRecord = null;
            if (identityRecord.Principal.HasValue 
                        && identityRecord.Principal.Value != Guid.Empty) {
                principalRecord = session.GetObjectByKey<Principal>(identityRecord.Principal.Value);
            }
            DateTime utcNow = DateTime.UtcNow;
            if (principalRecord == null) {
                principalRecord = new Principal(session);
                principalRecord.ID = Guid.NewGuid();
                principalRecord.Created = utcNow;
            }
            principalRecord.FullName = ticket.FullName;
            principalRecord.Email = ticket.Email;
            principalRecord.Modified = utcNow;
            /* Update Identity */
            identityRecord.Principal = principalRecord.ID;
            identityRecord.Modified = utcNow;
            /* Commit */
            principalRecord.Save();
            identityRecord.Save();
            session.CommitTransaction();
            status = MembershipCreateStatus.Success;
            ticket.Email = principalRecord.Email;
            ticket.Principal = principalRecord.ID;
            ticket.FullName = principalRecord.FullName;
            return ticket;
        }
    }
    

    What’s next

    Download source code for Part 4

    Cheers

    Azret

  • OData and OAuth – Part 3 - Federated logins with Twitter, Google, Facebook

    OAuth

    It’s time to look at OAuth protocol itself. In short, OAuth is a token based  authentication that became popular in recent years due to the adoption by big web giants like Google, Twitter, Facebook etc.

    There is a lot of good material on the web explaining the protocol flow in great details:

    http://oauth.net/about/ 

    http://hueniverse.com/oauth/ 

    http://www.viget.com/extend/oauth-by-example/

    http://dev.twitter.com/pages/auth 

     

     

    For us, it’s important because The Hobbit’s Bank Of the Shire exposes developer API and we want to make sure that any client app that is to use this API will not know the credentials of our members. And because everyone who lives in the Shire has an account on Twitter or Facebook and they would like to be able to sign into their accounts using those identities. Let’s start by adding support for federated logins: “Sign in with”

    When “signing in with”, we’ll redirect to the same Signin action on the AccountController with a parameter that tells us what service to use for authentication. Let’s start with Twitter (OAuth 1.0):

    <a href='@Url.Action("signin", "account", new { oauth_service = "tw" })' style="float:left; margin-left:10px;">
       <img style="float:left; border:0px; " src='@Url.Content("~/Content/Twitter.png")' alt="" />
    </a>
    public ActionResult Signin(Nullable<Guid> id) {    
        string service = Request.QueryString.Get("oauth_service");
        
        if (String.Equals(service, "tw", StringComparison.InvariantCultureIgnoreCase)) {
            return Redirect(
                AuthenticationService.GetAuthorizeUriForTwitter(Request.Url)
            );
        }
        
        MixedAuthenticationTicket ticket = MembershipService.CreateTicketForPrincipal(id);
        if (Impersonate(ticket, true)) {            
            return RedirectToAction("", "home");
        }
    
        return View(new Signin(MembershipService.GetPrincipals()));
    }

    AuthenticationService.GetAuthorizeUriForTwitter will make call to http://twitter.com/oauth/request_token to get the request token. Once the token is issued, it will return a URL to the http://twitter.com/oauth/authorize so the user can grant the access. (A complete OAuth flow for twitter is published here: http://dev.twitter.com/pages/auth). We’ll also need to register The Hobbit’s Bank Of the Shire with Twitter so that we are issued a Consumer Key and Consumer Secret: Twitter App Registration https://dev.twitter.com/apps/new.

    The Consumer Key and Consumer Secret is securely kept in the Web.config. Especially the Consumer Secret, as it’s used for signing the requests and only The Hobbit’s Bank Of the Shire and Twitter should know the value.

    <appSettings>
        <add key="Twitter_Consumer_Key" value="YOUR CONSUMER KEY" />
        <add key="Twitter_Consumer_Secret" value="YOUR CONSUMER SECRET" />
    </appSettings>

    Back to AuthenticationService.GetAuthorizeUriForTwitter, takes in a Uri parameter for the callback. We’ll use the same Signin action. Once redirected back, we’ll acquire an access token from twitter and sign in as that twitter user:

    public ActionResult Signin(Nullable<Guid> id) {
        string service = Request.QueryString.Get("oauth_service");
        if (String.Equals(service, "tw", StringComparison.InvariantCultureIgnoreCase)) {
            return Redirect(
                AuthenticationService.GetAuthorizeUriForTwitter(Request.Url)
            );
        }
    
        MixedAuthenticationTicket ticket = MixedAuthenticationTicket.Empty;
        string callback = Request.QueryString.Get("oauth_callback");
        if (String.Equals(callback, "tw", StringComparison.InvariantCultureIgnoreCase)) {
            ticket = AuthenticationService.FromTwitter(Request);
            if (Impersonate(ticket, true)) {
                return RedirectToAction("", "home");
            }
        }
        ticket = MembershipService.CreateTicketForPrincipal(id);
        if (Impersonate(ticket, true)) {
            return RedirectToAction("", "home");
        }
    
        return View(new Signin(MembershipService.GetPrincipals()));
    }

    image

    and we’re back on our property

    Google & Facebook

    We’ll follow the same pattern for Google and Facebook. Key difference is that Facebook uses OAuth 2.0 protocol. More here.

    public ActionResult Signin(Nullable<Guid> id) {
        string service = Request.QueryString.Get("oauth_service");
        if (String.Equals(service, "tw", StringComparison.InvariantCultureIgnoreCase)) {
            return Redirect(
                AuthenticationService.GetAuthorizeUriForTwitter(Request.Url)
            );
        } else if (String.Equals(service, "g", StringComparison.InvariantCultureIgnoreCase)) {
            return Redirect(
                AuthenticationService.GetAuthorizeUriForGoogle(Request.Url)
            );
        } else if (String.Equals(service, "fb", StringComparison.InvariantCultureIgnoreCase)) {
            return Redirect(
                AuthenticationService.GetAuthorizeUriForFacebook(Request.Url)
            );
        }
    
        MixedAuthenticationTicket ticket = MixedAuthenticationTicket.Empty;
        string callback = Request.QueryString.Get("oauth_callback");
        if (String.Equals(callback, "tw", StringComparison.InvariantCultureIgnoreCase)) {
            ticket = AuthenticationService.FromTwitter(Request);
        } else if (String.Equals(callback, "g", StringComparison.InvariantCultureIgnoreCase)) {
            ticket = AuthenticationService.FromGoogle(Request);
        } else if (String.Equals(callback, "fb", StringComparison.InvariantCultureIgnoreCase)) {
            ticket = AuthenticationService.FromFacebook(Request);
        } else {
            ticket = MembershipService.CreateTicketForPrincipal(id);
        }
    
        if (Impersonate(ticket, true)) {
            return RedirectToAction("", "home");
        }
    
        return View(new Signin(MembershipService.GetPrincipals()));
    }

    and we’re back on our property

    We now know how to do a “Sign in with”. All the Hobbits want to start using this feature right away. Unfortunately, we don’t have the ability to resolve federated identities back to our Principal object. We’ll cover this in the next post.

    Download source code for Part 3

    Cheers

    Azret

  • 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

  • OData and OAuth - Part 1 – Introduction

    I often receive questions about OData and authentication. Specifically, about OData + OAuth. The scenarios are very simple, you have a site/service and you want to provide developer API so that third-parties can integrate. The API must of course be secure, callers must properly be authenticated and only authorized resources/data must be available.

    Before we start going into details,  let’s build a sample site just so we have some context.

    image

    Our site we’ll be a front-end for the Hobbit's Bank Of The Shire Smile and we’ll use:

    The Site

    image

    The site would let members sing-in by presenting a list of all members, and a member would than click on himself to sign-in.

    image

    Members will be stored in the Principals table and represented by the XPO object Principal:

    [Persistent("Principals")]
    public class Principal : XPLiteObject {
        public Principal() { }
        public Principal(Session session) : base(session) { }
    
        [Key(AutoGenerate = true)]
        public Guid ID { get; set; }
    
        [Size(128)]
        public string FullName { get; set; }
    
        public byte[] Picture { get; set; }
        
        [Size(128)]
        [Indexed(Unique=true)]
        public string Email { get; set; }
        
        public DateTime Created { get; set; }
        public DateTime Modified { get; set; }
    }

    Once signed-in, a member would be able to see all the valuables in his safety-deposit box:

    image

    Valuables will be stored in the Valuables table and represented by XPO object Valuable:

    [Persistent("Valuables")]
    public class Valuable : XPLiteObject {
        public Valuable() { }
        public Valuable(Session session) : base(session) { }
        
        [Key(AutoGenerate = true)]
        public Guid ID { get; set; }
    
        [Size(128)]
        public string Name { get; set; }
    
        [Indexed(Unique=false)]
        public Guid Owner { get; set; }
    }

    The API

    Our data will be exposed using WCF Data Services using the XPO Data Service Provider:

    [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;
        }
    }

    Web.config

      <connectionStrings>
        <add name="BankOfShire" 
             connectionString="XpoProvider=MySql;Server=localhost;Database=BankOfShire;Uid=root;Pwd=password;" />
      </connectionStrings>
    
    
    XPO Data Service Provider lives in DevExpress.Xpo.v10.2.Data.Services so we’ll to reference that. And because we are using MySQL we’ll need:

    We can now make some OData calls from the browser:

    image

    Now that the foundation is laid in here is what to expect from this series:

    Download the source code for Part 1

    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