You will need a Microsoft Azure account. Create a new "Service Bus Namespace" (in this example it is "HaufeMessageBroker"). Under the "Configure" tab, add a new shared access policy, e.g. "OnPremiseRelay":
We make some assumptions about the on-premise API. These are not prerequisites in the sense that otherwise no access would be possible, but they should apply to many standard situations. It should also be fairly clear which aspects of the solution would have to be adapted to other situations.
- The on-premise API is an HTTP-REST-API.
- There is a Login-method, taking user name and password as parameters.
- The Login-method returns a Session-Id, which must be included in a header in all subsequent calls to the API to identify the user.
- The password is necessary in the on-premise API to identify the user, but it does not otherwise have an internal meaning.
- Counterexample: If the password is also necessary, for example, as credentials for a database login, then we have a problem.
- Reason: The solution binds an external identity (ASP.NET, Facebook, etc.) with the on-premise User-Id and allows the user to login with that identity, so the on-premise password is superfluous.
- Solution: If the on-premise password is actually necessary (e.g. for the database login), then it would have to be entered as part of or after the external login, which is of course possible but not really what we are hoping for in an SSO solution.
- The same API may be running on on-premise servers in different locations. For example a Lexware-API accessing the Lexware-Pro database would be running on every customer's server.
One easy way to create an on-premise API is using the self-host capabilities of ASP.NET with Owin. There are many how-tos available for doing this. However, this solution does not dictate how the on-premise API is to be implemented, and any one will do.
The Azure Service Bus provides an easy way to access an on-premise WCF (Windows Communications Foundation) interface from any cloud server. Of course, we do not want to rewrite our entire business API to become a WCF Interface, so part of the solution is to develop a small and generic WCF Interface, which resides in a new on-premise service and simply relays HTTP request/response information to and from the actual on-premise API. This is the "On-Premise Relay Service" below.
We also need two ASP.NET applications running in the cloud:
1. An ASP.NET web site ("Identity Portal") where a user can create a web identity (possibly using another identity like Facebook), then connect that identity to the on-premise login of the API running on his/her company's server. For this one-time action, the user needs to:
- enter a Host Id, which is the identification of the on-premise relay service running at his/her company location. This is necessary to tell the Azure Service Bus which of the many existing on-premise relay services this user wants to connect to.
- enter his on-premise user name and password. These get relayed to the on-premise API to confirm that the user is known there.
- From this time on, the web identity is connected to a specific on-premise relay service and to a specific on-premise identity, allowing SSO-access to the on-premise API.
2. An ASP.NET WebApi ("Cloud Relay Service") allowing generic access via the Service Bus to the on-premise API. This means, for example, that an application which consumes the on-premise API only need change the base address of the API to become functional through the Internet.
- Example: A browser app, which previously ran locally and called the API at, say:
- The only difference is that the user must first login using his web credentials instead of his on-premise credentials. The application then gets a token which identifies the user for all subsequent calls. The token contains appropriate information (in the form of custom claims) to relay each call to the appropriate on-premise relay service.
So there are actually two relays at work, neither of which has any business intelligence, but simply route the http requests and responses:
1. The ASP.NET WebApi "Cloud Relay Service", hosted in the cloud, which:
- receives an incoming HTTP request from the client, e.g. browser or smartphone app.
- converts it to a WCF request object, then relays this via the Azure Service Bus to the proper on-premise relay service.
- receives a WCF response object back from the on-premise relay service.
- converts this to a true HTTP response, and sends it back to the caller.
2. The "On-Premise Relay Service", which:
- receives an incoming WCF request object.
- converts it to a true HTTP request, then relays this to the endpoint of the on-premise API.
- receives the HTTP response from the on-premise API.
- converts it to a WCF response object and returns it via the Azure Service Bus to the ASP.NET WebApi "Cloud Relay Service".
In addition, there is the Azure Service Bus itself, through which the "Cloud Relay Service" and the "On-Premise Relay Service" communicate with each other.
Here we see a local client logging in to the on-premise API, thereby receiving a session-id, and using this session-id in a subsequent call to the API to get a list of the user's contacts.
After this process, the identity database contains additional information linking the web identity to a specific on-premise API (the "OnPremiseHostId") and to a specific on-premise identity (the "OnPremiseUserId"). From now on, whenever a client logs in to the ASP.NET Cloud Relay with his/her web credentials, this information will be added to the bearer token in the form of claims.
- The client first logs in to the ASP.NET Cloud Relay:
`https://lexwareprorelay.azurewebsites.net/api/account/externallogin` using its web identity credentials
- The client then logs in to the on-premise API:
`https://lexwareprorelay.azurewebsites.net/relay/account/v1/external_login` instead of `http://192.168.10.10/account/v1/login`
and does not include any explicit credentials at all, since these are carried by the bearer token.
- The client then makes "normal" API calls, with two differences:
- The base URL is now `https://lexwareprorelay.azurewebsites.net/relay/` instead of http://192.168.10.10/
- The client must include the authorization token (as a header) in all API calls.
What has changed for the on-premise API?
- The API provides a new method `accounts/v1/user_id` (used only once during registration!), which checks the provided credentials and returns the internal user id for that user. This is the value which will later be added as a claim to the bearer token.
- The API provides a new method `accounts/v1/external_login`, which calls back to the ASP.NET WebApi to confirm the user id, then does whatever it used to do in the original `accounts/v1/login` method. In this sample, that means starting a session linked to this user id and returning the new session-id to the caller.
- The other API methods do not change at all, though it should be noted that an authorization header is now always included, so that if, for example, the session-id should be deemed not secure enough, the on-premise API could always check the bearer token within every method.
The following sections show the actual code necessary to implement the above processes. Skip all of this if it's not interesting for you, but it is documented here to make the job easier for anyone actually wanting to implement such a relay.
Console.WriteLine("On-Premise Relay Service running...");
Console.ReadLine();
}
}
~~~
Notes:
- The hostId must be unique for each on-premise location.
- The service bus credentials (here, the name "haufemessagebroker" and the "OnPremiseRelay" must all be prepared via the Azure Portal by adding a new service bus namespace, as described in the introduction. In a live environment, you might want some kind of Service Bus Management API, so that each on-premise relay service could get valid credentials after, say, its company signed up for the relay service, and not have them hard-coded.
Once the on-premise relay service is running, you will see it listed with its host id in the Azure Management Portal under the "Relays" tab:
Create a new ASP.NET Project (named e.g. "IdentityPortal") and select "MVC". Before compiling and running the first time, change the class ApplicationUser (in `IdentityModels.cs`) as follows:
~~~csharp
public class ApplicationUser : IdentityUser
{
public string OnPremiseHostId { get; set; }
public string OnPremiseUserId { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
This adds two fields to the user identity, which we will need later to link each user to a specific on-premise API and specific on-premise user id. And, importantly, it adds the content of the two new fields as custom claims to the ApplicationUser instance.
By adding this code **before** running for the first time, the fields will automatically be added to the database table. Otherwise, we would need to add them as code-first migration step. So this just saves a bit of trouble.
Now compile and run, and you should immediately be able to register a new web identity and log in with that identity.
*Prepare to register with the on-premise API*
Use `NuGet` to add "WindowsAzure.ServiceBus" to the project.
Also, add a reference to the OnPremiseRelay DLL, so that the IRelay WCF Interface, as well as the Request and Response classes, are known.
In `AccountViewModels.cs`, add these classes:
~~~csharp
public class RegisterWithOnPremiseHostViewModel
{
[Required]
[Display(Name = "On-Premise Host Id")]
public string HostId { get; set; }
[Required]
[Display(Name = "On-Premise User Name")]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "On-Premise Password")]
public string Password { get; set; }
}
public class LoginCredentials
{
[JsonProperty(PropertyName = "user_name")]
public string UserName { get; set; }
[JsonProperty(PropertyName = "password")]
public string Password { get; set; }
}
~~~
In `_Layout.cshtml`, add this line to the navbar:
~~~html
<li>@Html.ActionLink("Register With Host", "RegisterWithOnPremiseHost", "Account")</li>
~~~
Add the following methods to the AccountController class:
Create a new ASP.NET Project (named e.g. "CloudRelayService") and select "Web Api".
- Before compiling and running the first time, make the same changes to the ApplicationUser class as mentioned above for the Identity Portal.
- Also, edit web.config and change the connection string for "DefaultConnection" to work with the same database as the Identity Portal by copying the connection string from that project.
- Important: if the connection string contains a `|DataDirectory|` reference in the file path, you will have to replace this with the true physical path to the other project, otherwise the two projects will not point to the same database file.
Add the following method to the AccountController (for this, you must include the System.Linq namespace):
- The note about the service bus credentials (in the on-premise relay service) applies here, too, of course.
The Cloud Relay WebApi should now be ready to return an authorization token for the web identity, and also relay http requests via WCF and the Azure Service Bus to the on-premise relay service.
Note that all relay methods are protected by the class's Authorize attribute.
*Examples using Chrome Postman:*
Get a token using a web identity (Note the path `/Token`, the content-type, and the content):