BlobShare Sample: ACS-Protected File Sharing
Weather is not that good this Sunday afternoon, and the wife warned me already yesterday that today she was going to catch up with the AI class; hence, I think I am going to break the blog-silence and spend some time describing BlobShare, a little jewel my DPE friends quietly released last week (and covered during the latest CloudCover episode, no pun intended).
BlobShare is a very nice Windows Azure sample, which demonstrates one way of solving a very concrete problem: how to share large files on the public internet, while maintaining full control over who can access what?
The usual disclaimers about a sample being a sample apply here, however you’ll be happy to know that DPE has been using an instance of BlobShare for sharing content for many months by now, it started while I was still over there. Many features in BlobShare derive from real usage requirements that emerged while actually using the application. I am so glad to see they finally managed to release it in a consumable form. Good job Wade’s gang!
Many of the things demonstrated in BlobShare have been featured in other samples: exhibit A, the email invitation system (seen in FabrikamShipping, the Umbraco ACS accelerator, etc). However it was always buried within many more moving parts, whereas here it is pretty easy to isolate. I am sure you’ll find it much easier to grok.
The same holds for various other aspects I am often asked about, like how to integrate an incoming IClaimsIdentity with attributes from a store which is local to the application: BlobShare does it to enable one of its key capabilities, enforcing locally stored permissions, hence the signal/noise ratio should be blindingly good.
In a nutshell:
- BlobShare is an MVC app which sits in front of your blob storage account
- The MVC app leverages ACS for admitting users with accounts from any of the identity providers it can trust…
- …but it maintains an application-local (SQL Azure) database of user profiles and roles.
- local user info and roles are used to establish if a given user has access to the blob he/she is requesting
- …but it maintains an application-local (SQL Azure) database of user profiles and roles.
Basically, all your blobs live in a trusted subsystem: only BlobShare can access blobs directly. BlobShare offers individual URLs for every blob, of course, but they are all rooted in the BlobShare app itself. As you try to get to one blob, BlobShare will funnel you through the authentication process and if it turns out you don’t have permissions for that specific blob, you don’t get access.
That’s pretty handy when you are sharing stuff you want to keep a close eye on: with a shared signature if the URL leaks you’re done, here not only you force authentication, you can even keep a useful audit trail and notice if there’s something fishy going on (if the same user accesses the same content in a small time interval and from many different machines, chances are you have somebody who’s sharing his account).
BlobShare offers a full UI for all the tasks that the flows introduced above entail. Administrators can upload blobs, group multiple blobs in larger sets, create users by sending them an invitation email, assign roles to users, permissions to roles and individual users, and examine a complete audit trail of all the users’ activities. Users can sign up (by responding to an invitation) and access the blobs for which they received permissions for, for as long as those permissions have been deemed valid.
Justo give you a feeling of the kind of things BlobShare keeps track of, below you can find the diagram of its SQL Azure database.
As you might notice, all the access control policy is kept in the database. Here ACS is being used to take care of authenticating with all the various trusted IPs: BlobShare’s setup will add the usual IPs, add the BlobShare instance as an RP, and create pass-through rules for all. BlobShare expects ACS to return a normalized token containing just the NameIdentifier and the IdentityProvider claims (take note if you later add custom providers). Those two claims are used to uniquely identify each user in the BlobShare database. There is no concept of pure, un-provisioned federated user here: if the incoming NameIdentifier-IdentityProvider tuple is not present in the db (and associated to the permissions of the blob being requested) the access is denied.
From the access control point of view, there are three (actually, four) different types of requests that are interesting to examine:
- Bootstrap, or Imprinting. The very first time a newly deployed BlobShare instance runs, it will onboard its first Administrator
- User redeeming an invitation. One new user received an invitation and is now going through the sign up flow
- User accessing a BlobShare URL that points to a blob.
- User signing in. Very similar to the above, sans interesting authorization tidbits
Instead of discussing those in abstract, we will explore those paths while walking through some typical application use. In a minute.
As you know I have this unhealthy passion for putting together pictures which show as many things at the same time as I can fit in the allotted real estate. That’s great when you already understand the matter, I find it helps me to understand relationships and an architecture as a whole; but while you are learning it might not offer the most gentle slope for ramping up.
Well, below you can find one such diagram: it lists the relevant moving parts in BlobShare that come into play when a request carrying a token shows up.
Please ignore the details of the flows for now, you can come back to this figure every time we’ll delve in the details of those. The thing I’d like you to observe at this time is how the solution is layered in various elements, each taking care of a specific access control task:
- WIF sits in front of the application
- The first layer implements the classic mechanisms of federated authentication: forward the user to the identity provider (or to a broker like ACS, in this case) according to the protocol of choice, verify that incoming tokens are well-formed/have not been tampered with/are not expired/come from the expected authority and so on
- The next layer is a ClaimsAuthenticationManager implementation. In BlobShare this an especially important stage, as so much information that is relevant to the incoming user’s identity resides in the RP’s database and needs to be reconciled before the call can go any further. Moreover, which information is relevant changes dramatically between call types (more below)
- The last layer before giving control to the application is an implementation of a ClaimsAuthorizationManager. This is a cornerstone in BlobShare, as it represents the enforcement stage for the policies defined through the application flow
- The application itself will use the incoming claims to customize what is shown (i.e. every user will see only the blobs he/she has access to) and for auditing purposes
IN the rest of the post I’ll go through BlobShare doing some basic tasks. As the elements and the different requests types come into play I’ll add commentary & take the chance to dig deeper in some of those concepts.
As I mentioned earlier, those tasks correspond to questions I get very often, hence I suspect that some of you guys will really like some of this stuff
Handling the administrator of those sites that use federated identity is always an interesting challenge. How to ensure that once you deployed your site, the administrator can log in and start working right away?
Adding a username/password is kind of bad form. Every user can reuse existing social accounts, why should the admin be left out? On the other hand, if you want to authenticate the admin with a social account you have the following problems:
- at design time you might not know the values (like the nameidentifier) that will be in the authentication token the admin will send. Without those values how can you tell if the incoming user is really the intended admin?
- you might not even know which identity provider the admin will want to use: live id? Google? who knows.
Sure, one could use also with the admin the classic email invitation flow: after all, we are using it for other users right? Unfortunately it might not be a good idea to take a dependency on a setting that the admin itself might be required to provide (in BlobShare the SMTP server is already set up, but it’s just coincidence: I think that the original plan was to make it configurable on first run).
However, even if the probability that a random dude beats you to your deployment, it was still a possibility: hence we devised a schema for which at deployment time you establish a secret, and you are required to provide that secret on first run to associate your social account to the administrator user of that BlobShare’s instance.
Easier to show that to describe! Hit F5 on your local instance, you’ll be brought straight to the page below.
At the time this page was called sign up: I am not sure why the guys decided to change it, but it works nonetheless. Just sign in using whatever account you prefer.
You’ll go through the usual dance with your IP and ACS, then land on the page below.
Add the email you want to use, provide the secret and…
…the duckling will think you are Momma, and from now on you’ll have admin access to BlobShare.
How did this happen? Didn’t I say above that one user needs to be in the database in order to have access? Yes I did, but AccountAssociationClaimsAuthenticatonManager makes an exception for the case in which the database has exactly zero users. It even creates a new user for the occasion! The secret verification takes place in the associated controller (point (A ) in the uber-diagram).
Now that we are admin, we can start to play. Let’s sign in.
..and we’re in (BTW: wow, this sample looks great. The designers did an excellent job).
You might not see anything out of the ordinary, but that’s mostly because you didn’t see that I used Live ID for signing in. Live ID does not give any claims besides the nameidentifier, whereas in the screenshot below BlobShare is clearly greeting me with something else (my email).
That’s because AccountAssociationClaimsAuthenticatonManager graciously recognized me as an existing user of BlobShare, hence retrieved my extra attributes (Name and email, which in this case are both set to the email value) and used them to augment the claims already in the existing ClaimsPrincipal.
Let’s go under Blobs.
Hmm, it’s pretty barren here. Let’s click on Upload, then Single File Upload.
Let’s add a picture I am sure I own the rights for, and jolt down some comments just for color.
…and here there’s our first blob.
Let’s hit on Permissions.
Handling Access Rights and Users
Here I can grant access to this blob for users or roles, but I have none for now (apart from myself and the admin role, who already does have access).
Let’s add a new role, then. Click on Roles on the top bar.
Click new, and you get to the simplest role creation form you’ve seen to date. Once you’re done hit Update.
Excellent, we have a role now; but no user to assign this to. Let’s go to Users by clicking the associated entry in the top bar.
Here there’s the list of our users, currently including only myself. I could add many at once, but for the sake of demonstration I’ll create just one. Hit on Invite User.
(note: all those controllers, which require administrative privileges, are decorated by a custom AuthorizeAttribute which enforces things accordingly).
The matter is pretty simple: you specify an email address to send the invite to, and you decide which roles the new guy will belong to. Hit Create and you’re done for now.
The user has been created in BlobShare’s database, and an email with the invitation has been sent; however until Adam has not accepted, we don’t know which nameidentifier should be associated to this profile (nor from which identity provider we should expect Adam to come from).
What happened is that BlobShare created a unique ID associated to this profile. That ID will be embedded in one registration URL that Adam will receive in the invitation; whomever will present a token through that URL will become Adam as far as BlobShare is concerned.
Accepting an Invitation
Let’s take a look at things from Adam’s perspective.
Adam receives the invitation mail below. The mail contains the mentioned invitation URL; nobody but Adam (or better, whomever has access to the mailbox specified at invitation time) know this URL, which is a pretty good way to be reassured that we are inviting the right person. Let’s click on the URL.
Here we once again encounter the sign-in page (again, at the time it was supposed to say sign-up and some helpful text, but that’s a technicality). Adam can use whatever account from the listed providers: that account will become Adam for BlobShare. Once again, take a look at AccountAssociationClaimsAuthenticationManager to see how the reconciliation happens.
Once the profile-token reconciliation took place, you can even update some values (like the name that was originally specified when creating the invitation).
Once signed in (again) and gone to Blob, Adam will find that there are still no blobs he can see. Let’s leave Adam for a moment and go back to the administrator’s experience.
Assigning Roles & Permissions
If you refresh Adam’s page, you’ll see that the user is now active and the attributes all have the correct values.
Now that our Colleagues role is non-empty, we can get back there and assign some permissions.
And here it is: the Colleagues role has been granted Read access to our only blob. Note that I could have granted access directly to Adam instead of the group he belongs to; or that I could have put my blob in a blob set and handled access to the set rather than the individual blob. BlobShare is VERY flexible.
It is worth stressing that all those changes are happening in the SQL Azure database, not in ACS: no new rules are being written. In BlobShare all settings are at the RP side.
Accessing a File
Let’s get back to Adam. If Adam hits F5, the browser will refresh & show the newly granted blob.
Adam can hit both the name of the file in the blob (Phoenix in the sample) or Download. Access-wise there is no difference, this only impacts how the file will be served. Clicking on the file will show it in the browser, as you can see below (yes, that was a LONG meeting).
Now, until now I omitted to mention the BlobAuthorizationManager. The reason is that the guy steps out of the way every time the request is going to a controller other than MyBlobs. If it is MyBlobs, as it is the case now, it queries the db (via a service) to ensure that the user has the rights to access the blob he is requesting. Check out the code, it’s very nicely readable.
Let’s get back to the administrator for one more thing. If you click on Reports, you’ll land on the page below.
Let’s click on User Activity; we’ll see all the things we’ve been doing until now with BlobShare, which is quite handy. If you look at the code, you’ll see that the current ClaimsIdentity is used across the board for retrieving the user info in a nice, consistent way, regardless of whether they come from the identity provider or they have been extracted from the RP database.
Well, the afternoon kind of stretched well into the evening but BlobShare is a great sample, and I think it deserves all the coverage it can get.
If you are into WIF, this is a great sample that demonstrates how to take advantage of the main extensibility points. Do play with the code, and if you have questions or feedback I am sure that the Wade gang will be delighted to hear you out. If you want to chat about the identity side of things, I am happy to chat as well but I can’t take feature requests, that’s Wade’s jurisdiction