Dissecting the Web API Individual Accounts Template–Part 3: External Accounts

Part 1 covered the basic template security setup. Part 2 focused on the features around local accounts and username/password authentication. This part will deal with third party authentication using Google and friends as well as associating a third party account with a local account.

Social providers typically don’t offer a web service interface for authentication (for good reason), but rather an authentication UI wrapped inside some protocol like OpenID (Connect) or authentication flavoured OAuth2. That means the client application has to use a browser to render those UI parts – typically the login page and some type of consent screen.

This normally works by navigating to a well known URL (the Web API template uses the OAuth2 implicit flow approach) and waiting ‘til a callback on some other well known URL happens. On the callback the access token (plus some metadata) is attached via a hash fragment. What’s happening in between is total up to the authorization server and its coordination with the external login provider. The client does not need to know how that works, which is nice.

The Individual Accounts account controller features an endpoint that queries the registered middleware and tells the client which external providers are available and which URLs it has to use to get the flow going. This information can be used to shape the UI of the client application:

private async Task GetExternalLoginsAsync()

{

    var client = new HttpClient { BaseAddress = new Uri(_baseAddress) };

    var response = await client.GetAsync(
     
“api/account/externalLogins?returnUrl=/&generateState=true”
);

    response.EnsureSuccessStatusCode();

 

    _externalLogins = await response.Content.ReadAsAsync<
     
List<ExternalLoginViewModel
>>();

}

The ExternalLoginViewModel is fairly simple:

public class ExternalLoginViewModel

{

    public string Name { get; set; }

    public string Url { get; set; }

    public string State { get; set; }

}

Url is the start URL (the callback URL is hardcoded to the base address in the template) and State is a random number that gets round tripped and should be checked on the callback (see the OAuth2 spec for details). The Url looks like this (sample):

/api/Account/ExternalLogin?
  provider=Google&
  response_type=token&
  client_id=self&
  redirect_uri=https://localhost/&
  state=as..aaa

The /api/account/externalLogin endpoint maps to the GetExternalLogin action on the account controller which has quite an interesting combination of attributes:

[OverrideAuthentication]

[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]

[AllowAnonymous]

[Route(“ExternalLogin”, Name = “ExternalLogin”)]

This basically means: don’t care about bearer tokens but allow anonymous access or an external cookie (see part 1).

In addition this endpoint also maps to the authorize endpoint of the OAuth2 authorization server middleware (check startup) which is kind of a black belt trick. First the middleware processes the request and then passes it on to whatever framework handles the URL – in this case Web API.

The exact course of events is quite involved. Let’s start with the moment when the web view hits the URL for the first time (check out the source code in parallel):

  1. Access /api/account/externalLogin?provider=Google…
    1. the user is not authenticated
    2. signal the middleware that is responsible for the requested external provider to do the protocol handshake (in this case Google)
  2. The user signs in at Google and does the consent
    1. Google calls back to the Web API using the /signin-google URL
  3. The Google middleware does its back channel communication and sets an external cookie containing some of the claims that came back from Google
    1. the middleware then redirects back to the /api/account/externalLogin endpoint
  4. This time the user is authenticated via the external cookie
    1. The account controller now checks if the login provider / user id combination is already registered in the local database
    2. this is not the case, so the account controller passes control back to the authorization server middleware instructing it to issue a token that contains the claims from Google (external bearer – the claims issuer is Google – again – check part one for the subtle meaning of that).
  5. The callback URL is invoked, and the client application retrieves the access token

At this point the client app has an access token, but it won’t get accepted by default since the claims are not issued by LOCAL AUTHORITY but Google instead. At this point you could open up endpoints by adding:

[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]

One endpoint that is configured exactly like that is the /api/account/registerExternal. This endpoints accepts the token you get from the previous step and allows it to associate with a new local account (without a password):

private void RegisterExternal_Click(object sender, RoutedEventArgs e)

{

    var client = new HttpClient { BaseAddress = new Uri(_baseAddress) };

    client.SetBearerToken(_externalResponse.AccessToken);

 

    var data = new Dictionary<string, string>

    {

        { “UserName”, “dominick” }

    };

 

    var response = client.PostAsync(
     
“api/account/registerExternal”,
     
new FormUrlEncodedContent
(data)).Result;

    response.EnsureSuccessStatusCode();

}

RegisterExternal uses the ASP.Identity API to create a new user account and associates that with the external provider and user id from the token.

Now the last step is to go back to /api/account/externalLogin to get a new token that now represents a local account (or technically speaking claims from LOCAL AUTHORITY). The flow is like this:

  1. Invoke externalLogin with the same URL as before
    1. the user is still authenticated using the same external cookie
    2. the login provider / user id pair is now registered
  2. The controller clears the external cookie
    1. and creates a new authentication ticket
    2. this ticket translates to a new token with an issuer of LOCAL AUTHORITY – aka local account
  3. The callback URL transmits the token back to the client

Mission accomplished – the client used an external login provider to authenticate the user and linked that login to an account in its backend. Quite a stunt.

HTH

This entry was posted in ASP.NET, Katana, OAuth, OWIN, WebAPI. Bookmark the permalink.

36 Responses to Dissecting the Web API Individual Accounts Template–Part 3: External Accounts

  1. The whole OWIN arrangement feels like a rewrite of the adaption layer the previous version of ASP.NET used, to interface to dotnetopenauth libraries (and their approach to componentized oauth).

    Instead of RPC/XDR (Xerox era), we got SOAP/XML (web era). Tw architectures doing the same thing, the same way, but with some differences in the space of things that may be actually built.

  2. Cyril says:

    Step 1 isn’t even working for me! I tried calling a GET request to /api/account/externalLogin?provider=Google from fiddler and/or my browser and it gives me a 400 bad request error. Calling /api/account/externalLogins?returnUrl (mapped to getExternalLogins) works. So i’m thinking this has to do with the auth server? because it is also mapped to /api/account/externalLogin ?

    I’m working in an empty webapi template with individual authentication. All i did was uncomment the google external authentication.

    Any idea?

    Thank you.

    • Have a look at the SPA template – it is essentially the same back-end but with a JS front end – you can easily follow the flows with fiddler or browser tools.

      • bartek4c says:

        One more question: see at point 3) The callback URL transmits the token back to the client (it’s the second token with claims from local authority). How does the javascript know that this is the token for registered user. I’ve analysed both tokens (for unregistered user, and for registered one) and both of them have the same structure?

    • Jeremy says:

      I ran into the same problem, and found that the issue was that the URL returned by GetExternalLogins does not contain a trailing slash before the parameters. That is, if you change the URL to /api/account/externalLogin/?provider=Google (note the forward slash right before the question mark) it will work. I have not had any luck whatsoever with fixing this though, and would appreciate any ideas. Hope this helps!

  3. I need to be able to support local account users as well as authenticating against users in Active Directory. Would the individual Accounts Template be the starting point for this? My thoughts would be to use Thinktecture AuthorizationServer as the “relying party to some WS-Federation identity provider”, in my case ADFS.

    If not can you point me in the right direction?

    Thanks in advance
    Christian Crowhurst

  4. Sorry forgot to say, I also need to associate the AD user with a local user account (in the same way you describe above). That way I can build an additional set of claims from information associated with the local user maintained in the local database.

  5. bartek4c says:

    I follow your post up to ‘5) The callback URL is invoked, and the client application retrieves the access token’ and everything is clear up to then. However, I call Web API from external MVC application, and after authenticating by Google, would like the user to be redirected back to registration page to fill in all additional details. Only after that, I would like to call api/account/RegisterExternal and pass the complete view model for user registration. How can I redirect user back to the registration page, and retrieve Google access token details at the same time?

  6. I think you can’t because the Web API template is not built for web clients – e.g. the OAuth2 implicit flow that is used there transmits the token via a hash fragment. That would not travel to a server.

    You rather want to use the MVC template – which is built for your purpose.

    • bartek4c says:

      The thing is, my scenario requires MVC application to be hosted outside the LAN, and the API, together with all user details, within it. Are there any options to retrieve token from the hash or pass it using any other way? Do I have any other choices in a given scenario, if I would like to support external login providers?

      • bartek4c says:

        The thing is, my scenario requires MVC application to be hosted outside the LAN, and the API, together with all user details, within it. Are there any options to retrieve token from the hash or pass it using any other way? Do I have any other choices in a given scenario, if I would like to support external login providers?

        UPDATE I found a temporary solution by using javascript to process url hash and call external application. Still cannot get the token itself, since /Token endpoint requires username/password pair. Also, I imagine this approach is not very secure as well?

        Please help!!!

  7. For server side apps – the code flow is more appropriate. I don’t have a sample for that for web api v2 though.

    • bartek4c says:

      Knowing the case and being aware that I’m no expert in the area how would you suggest I should approach my scenario. I’m obviously not capable of writing my own authorization server, and would like to use already existing one – MS preferably.

    • bartek4c says:

      Two questions then:

      1) From the description: ‘AS deliberately doesn’t do authentication. It solely focuses on authorization’ – I do not really use claims in my scenario, since it’s designed for only one type of users, therefore any authenticated user would be automatically authorized to use the service. Also, would really like to support SSO – so what would you recommend for an authentication service when using Thinktecture AS?

      2) I’ve seen your Thinktecture Identityserver 2 – would it be of any use for me? Does it support SSO? Also, as I read it ‘has out-of-the-box support for user management using the ASP.NET membership and roles provider’ – it is not OWIN friendly then, isn’t it?

  8. If your OAuth2 requirements are simpler – the IdSrv will do just fine – and you have the built-in authentication system.

    I don’t get the comment about OWIN friendly – could you explain?

    • bartek4c says:

      As I wrote, the documentation for IdServ says ‘has out-of-the-box support for user management using the ASP.NET membership and roles provider’. I thought that MS membership has been replaced by Identity, which brakes dependency on System.Web, therefore is OWIN friendly?

      • Well MVC is not OWIN friendly either – still not sure what your point is.

        IdSrv uses ASP.NET membership by default – but you can replace authentication mechanism very easily – check the wiki for an “extending idsrv” article.

  9. bartek4c says:

    Well, I’ll give it a try and see where it’ll get me. By the way, thanks very much for your help, I really appreciate it.

  10. It doesn’t – the access token is opaque to clients.

  11. Pingback: The Web API v2 OAuth2 Authorization Server Middleware–Is it worth it? | leastprivilege.com

  12. supperslonic says:

    Hi!
    Do you know how to obtain a refresh token for the external providers?

  13. Hi Dominick, appreciate your efforts explaining the individual accounts and external accounts in Web API template.
    I’ve question here, can I use the end point “RegisterExternal” without depending on external cookies? Is it possible to get the “ExternalLoginInfo” as the line below from the token not from the cookie?
    var info = await Authentication.GetExternalLoginInfoAsync();

  14. Hi Dominick,
    I’m trying to add external logins to the Web API I’ve built here: https://github.com/tjoudeh/AngularJSAuthentication/tree/master/AngularJSAuthentication.API
    The front-end angularjs app is separate app hosted on different domain other than the back-end api. I’m trying to add/enable manually Google external login in the back-end api.
    The problem I’m currently facing that when I’m issuing GET request to the end point “/api/account/ExternalLogin” everything works correctly except the final part which is the redirection (issuing 302 response) to my front end application, so currently I’m not getting the access_token fragment.
    I believe it is working seamlessly on the SPA template and Web API/mvc template with individual accounts because the web app is living within the back-end api.

    It will be great if you have any thoughts or advice on this.

    • jjpmcd@gmail.com says:

      Hi Taiseer,
      I’ve been working through the same External Login flow after reading your excellent three-part guide.

      I might be wrong, but I think for the final part to work you need to have this setting in your OAuthAuthorizationServerOptions

      AuthorizeEndpointPath = new PathString(“/api/Account/ExternalLogin”),

      I read it in this article MSDN article “Understanding Security Features in the SPA Template for VS2013 RC” where there is a good description under “External login flow”
      http://blogs.msdn.com/b/webdev/archive/2013/09/20/understanding-security-features-in-spa-template.aspx

      I haven’t implemented it myself just yet, but I have spent a lot of time pouring over the templates and various articles. Good luck!

  15. James McDonnell says:

    Hi Taiseer,
    I’ve been working through the same External Login flow after reading your excellent three-part guide.

    I might be wrong, but I think for the final part to work you need to have this setting in your OAuthAuthorizationServerOptions

    AuthorizeEndpointPath = new PathString(“/api/Account/ExternalLogin”),

    I read it in this article MSDN article “Understanding Security Features in the SPA Template for VS2013 RC” where there is a good description under “External login flow”
    http://blogs.msdn.com/b/webdev/archive/2013/09/20/understanding-security-features-in-spa-template.aspx

    I haven’t implemented it myself just yet, but I have spent a lot of time pouring over the templates and various articles. Good luck!

  16. Pingback: ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app - Bit of Technology

  17. michael luce says:

    Hi,

    I’m trying to implement this with a Windows Phone app and WebAPI but keep getting ‘Unauthorized’ from my web API after obtaining the facebook access token. I think my process might be missing a step – do you have an accompanying solution example here? I notice it says to follow along with source code but cannot find a link.

    Thanks!

  18. Kert says:

    Hi,

    I have the same question that “bartek4c” had.

    The callback URL transmits the token back to the client (it’s the second token with claims from local authority). How does the javascript know that this is the token for registered user? I need to display registration form only when needed.

  19. Hi,
    In the AccountController the ExternalLoginCallback gets called once the user successfully logs into the Google account – but how would one exchange the code for the token. I’m using Web Api v2. I’ve also looked at your excellent videos on OAuth on Pluralsight but it some how I cannot seem to see the code where the code is extracted and exchanged for the token and the refresh token

  20. Lisa says:

    Hi Dominick,

    Thank you for providing this blog to the community as I have found it help.

    I’m currently working on a project where I have an MVC project (this is mvc with individual account) which serves as my client side/user interface only. The MVC project sends requests to my Web Api project (this is web api with individual user account) to do the work (insert records, get records, insert users, authenticate users, etc).

    Currently I’m having problems with getting the external logins working properly on the web api side. I want the web api to do the authentication instead of the MVC.

    On the MVC client side, I’ve retrieved the URL of the external provider I want to use, I then send a GET request to the URL of the external provider. This get request hits the GetExternalLogin method on the web api which is what I want to happen because I want the web api to do the authentication (unless what I currently have setup is not proper architecture, please let me know).

    So far this hits the “return new ChallengeResult(provider, this);” in

    // GET api/Account/ExternalLogin
    [HttpGet]
    [OverrideAuthentication] // override global setting and only accept an application cookie
    [HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
    [AllowAnonymous]
    [Route(“ExternalLogin”, Name = “ExternalLogin”)]
    public async Task GetExternalLogin(string provider, string error = null)
    {
    if (error != null)
    {
    return Redirect(Url.Content(“~/”) + “#error=” + Uri.EscapeDataString(error));
    }

    if (!User.Identity.IsAuthenticated)
    {
    return new ChallengeResult(provider, this);
    }
    ……
    }

    I then end up with a blank white screen and the url on the screen is

    https://localhost:44300/Account/ExternalLogin

    (https://localhost:44300 is the url of the MVC project and /Account/ExternalLogin is the method on the MVC client side).

    Fiddler captured a 302 response (which I understand is how this should be):

    GET /o/oauth2/auth?response_type=code&client_id=51581202790-7hj5vr2f1e4ns25cr6i4jvmn4e4jqg8i.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Flocalhost%3A44301%2Fsignin-google&scope=openid%20profile%20email&state=RnwnkzGrvRB_CqL-wsNtUD1nYeizKCKn1cJCNYbHPRC38DqNyCn_OzwuVOYurp5fwZbB52dp5XdDlHMlDhUtjHKf6njku-jEW6s9unjRR3IsC50qNqTSl5NpFdZkVw_kAN2YYqgj37zLyrnhqpcbSSp4U_7KHLsEnDLGPCIbe24DW3cDwAB6hhAp9SfvZO_dOhwVOCp6iv4cPLFwSPnzLw HTTP/1.1

    This is the first time I’m working with ASP Identity, OWIN and Katana technology and perhaps I’m not understanding how this works. I’m a bit stumped. I’ve been doing quite a lot of googling and watching videos on Pluralsight and reading up on the material but I have not been able to find what I’m looking for.

    Any help you could offer would be greatly appreciated.

    Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s