Registering Web API 2 external logins from multiple API clients with OWIN Identity

joshcomley picture joshcomley · Jan 16, 2014 · Viewed 25.9k times · Source

I would like the following architecture (I've made up the product name for this example):

Web API 2 application running on one server http://api.prettypictures.com

MVC 5 client app running on another server http://www.webpics.com

I would like www.webpics.com client app to use the Pretty Pictures API to:

  • Register new accounts with username and password
  • Register new accounts with Facebook/Google/Twitter/Microsoft
  • Log in
  • Retrieve pictures

All of the above works except registering external accounts with Facebook, Google etc.

I cannot work out the correct flow to create an external account from a separate client user of the API.

I have studied most documents available on the authentication flow, like this: enter image description here

I have read pretty much everything I can on the new Identity model in OWIN.

I've examined the SPA template in Visual Studio 2013. It demonstrates how to do most of what I need but only when the client and the API are on the same host; if I want multiple clients accessing my API and being able to let users sign up via Google etc. it doesn't work and as far as I can tell the OWIN authentication flow breaks.

Here is the flow so far:

  • User browses to www.webpics.com/Login
  • www.webpics.com calls api.prettypictures.com/Account/ExternalLogins (with a returnUrl set to go back to a callback at www.webpics.com) and displays the resulting links to the user
  • The user clicks "Google"
  • The browser redirects to api.prettypictures.com/Account/ExternalLogin with the name of the provider etc.
  • The API's ExternalLogin action instantiates a challenge to google.com
  • The browser is redirected to google.com
  • The user enters their username and password (if they are not already logged in to google.com)
  • google.com now presents the security clearance: "api.prettypictures.com" would like access to your email address, name, wife, children etc. Is this OK?
  • User clicks "Yep" and is taken back to api.prettypictures.com/Account/ExternalLogin with a cookie that Google has set.

This is where I've got stuck. What is supposed to happen next is somehow the client app should be notified that the user has successfully authenticated with google.com and be given a single use access code to swap for an access token later on. The client app should have the opportunity, if necessary, to prompt the user for a username to associate with their google.com login.

I don't know how to facilitate this.

In fact at this point the browser ends up sat at the api.prettypictures.com/Account/ExternalLogin endpoint after the callback from Google. The API is signed in for Google but the client doesn't know how to deal with that. Should I pipe that cookie back to www.webpics.com?

In the SPA app, it is done via AJAX and google.com will return an token as a URL fragment and it all works nicely because it all sits on one domain. But that defies much of the point of having an "API" that multiple clients can fully use.

Help!

Answer

Kévin Chalet picture Kévin Chalet · Jan 22, 2014

Update: things have changed since I wrote this post in January: MSFT released their official OpenID connect client middleware and I worked hard with @manfredsteyer to adapt the OAuth2 authorization server built in Katana to OpenID connect. This combination results in a far easier and far more powerful solution that doesn't require any custom client code and is 100% compatible with standard OAuth2/OpenID connect clients. The different steps I mentioned in January can now be replaced by just a few lines:

Server:

app.UseOpenIdConnectServer(options =>
{
    options.TokenEndpointPath = new PathString("/connect/token");
    options.SigningCredentials.AddCertificate(certificate);

    options.Provider = new CustomOpenIdConnectServerProvider();
});

Client:

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
    Authority = "http://localhost:55985/",

    ClientId = "myClient",
    ClientSecret = "secret_secret_secret",
    RedirectUri = "http://localhost:56854/oidc"
});

You can find all the details (and different samples) on the GitHub repository:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/tree/dev/samples/Nancy


Josh, you're definitely on the right track and your delegated/federated authentication implementation seems pretty good (I imagine that you've used the predefined OWIN middleware from Microsoft.Owin.Security.Facebook/Google/Twitter).

What you need to do is creating your own custom OAuth2 authorization server. You have plenty of options to achieve that, but the easiest one is probably to plug the OAuthAuthorizationServerMiddleware in your OWIN Startup class. You'll find it in the Microsoft.Owin.Security.OAuth Nuget package.

While the best practice would be to create a separate project (often called "AuthorizationServer"), I personally prefer adding it to my "API project" when it is not meant to be used across multiple API (here, you would have to insert it in the project hosting "api.prettypictures.com").

You'll find a great sample in the Katana repository:

https://katanaproject.codeplex.com/SourceControl/latest#tests/Katana.Sandbox.WebServer/Startup.cs

app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
    AuthorizeEndpointPath = new PathString("/oauth2/authorize"),
    TokenEndpointPath = new PathString("/oauth2/token"),
    ApplicationCanDisplayErrors = true,

    AllowInsecureHttp = true,

    Provider = new OAuthAuthorizationServerProvider
    {
        OnValidateClientRedirectUri = ValidateClientRedirectUri,
        OnValidateClientAuthentication = ValidateClientAuthentication,
        OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
    },
    AuthorizationCodeProvider = new AuthenticationTokenProvider
    {
        OnCreate = CreateAuthenticationCode,
        OnReceive = ReceiveAuthenticationCode,
    },
    RefreshTokenProvider = new AuthenticationTokenProvider
    {
        OnCreate = CreateRefreshToken,
        OnReceive = ReceiveRefreshToken,
    }
});

Don't hesitate to browse the whole project to see how the authorization consent form has been implemented using simple Razor files. If you prefer a higher-level framework like ASP.NET MVC or NancyFX, create your own AuthorizationController controller and Authorize methods (make sure to accept both GET and POST) and use Attribute Routing to match the AuthorizeEndpointPath defined in your OAuth2 authorization server (ie. [Route("oauth2/authorize")] in my sample, where I've changed the AuthorizeEndpointPath to use oauth2/ as a path base).

The other thing you need to do is adding an OAuth2 authorization client in your web app. Unfortunately, there's no generic OAuth2 client support in Katana, and you'll have to build your own. I've personally submitted a proposal to the Katana team, but it has been refused. But don't panic, it's rather easy to do:

Copy the appropriate files from the Microsoft.Owin.Security.Google repository located there: https://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Google/GoogleOAuth2AuthenticationHandler.cs

You'll need GoogleOAuth2AuthenticationHandler, GoogleOAuth2AuthenticationMiddleware, GoogleOAuth2AuthenticationOptions, GoogleAuthenticationExtensions (you'll have to remove the first 2 methods corresponding to the Google OpenID implementation), IGoogleOAuth2AuthenticationProvider, GoogleOAuth2ReturnEndpointContext, GoogleOAuth2AuthenticationProvider, GoogleOAuth2AuthenticatedContext and GoogleOAuth2ApplyRedirectContext. Once you've inserted these files in your project hosting "webpics.com", rename them accordingly and change the authorization and access token endpoints URL in GoogleOAuth2AuthenticationHandler to match the ones you've defined in your OAuth2 authorization server.

Then, add the Use method from your renamed/custom GoogleAuthenticationExtensions to your OWIN Startup class. I suggest using AuthenticationMode.Active so that your users will be directly redirected to your API OAuth2 authorization endpoint. Thus, you should suppress the "api.prettypictures.com/Account/ExternalLogins" roundtrip and let the OAuth2 client middleware alter 401 responses to redirect the clients to your API.

Good luck. And don't hesitate if you need more information ;)