Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing External Authentication for Mobile App in ASP.NET WebApi 2

I'm trying to build an API (using ASP.NET WebApi) that will be consumed by a native mobile app for a school project. (I'm not concerned about/developing the mobile app, this responsibility falls on a different member) I'm at a point where I need to implement a token based Facebook login. There are a lot of tutorials available for how to implement this feature for browser based apps (this is pretty straight forward and most of it comes inbuilt), but I don't think I follow how this would work with native apps. What I don't understand is how the redirects would work?

According to this link, nothing needs to be handled specifically by my server. And I don't think I understand how this would work? How would the tokens from Facebook be handled?

Also, what part of token handling should I implement, I couldn't really find good documentation for WebApi external login authentication.

Anyway, if someone could point me to the exact flow of token exchanges that happen and what is implemented by default by ASP.NET, that would be super helpful.

Also, the biggest point of confusion for me is I don't understand how the token returned by Facebook will be handled.

  1. I assume the token will be returned to the client (mobile app), how do I get access to it on my server?
  2. How do I create a local token from facebook's token? Is this all done internally/auto-magically by ASP.NET?

I'm sorry if this is something I should've been able to figure out. I did do quite a bit of research and I found myself drowning in (related & unrelated) information. I don't think I even know how to search for the information I need.

Some links I've read:

Claims And Token Based Authentication (ASP.NET Web API)

Token Based Authentication using ASP.NET Web API 2, Owin, and Identity

ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app

like image 392
Chris Turk Avatar asked Nov 16 '15 01:11

Chris Turk


People also ask

How will you implement authentication and authorization in ASP.NET web API?

Web API assumes that authentication happens in the host. For web-hosting, the host is IIS, which uses HTTP modules for authentication. You can configure your project to use any of the authentication modules built in to IIS or ASP.NET, or write your own HTTP module to perform custom authentication.

What is OAuth 2.0 authentication in web API?

OAuth2 is the preferred method of authenticating access to the API. OAuth2 allows authorization without the external application getting the user's email address or password. Instead, the external application gets a token that authorizes access to the user's account.

What is external authentication in web API?

The user agent sends its credentials to the external authentication service, and if the user agent has successfully authenticated, the external authentication service will redirect the user agent to the original web application with some form of token which the user agent will send to the web application.

How do I add authentication to web API?

In IIS Manager, go to Features View, select Authentication, and enable Basic authentication. In your Web API project, add the [Authorize] attribute for any controller actions that need authentication. A client authenticates itself by setting the Authorization header in the request.


2 Answers

I had to do pretty much the same thing for an application I was working on. I also had a lot of trouble finding information about it. It seemed like everything I found was close to what I needed, but not exactly the solution. I ended up taking bits and pieces from a bunch of different blog posts, articles, etc. and putting them all together to get it to work.

I remember two of the links you posted "Claims and Token Based Authentication" and "ASP.NET Web API 2 external logins with Facebook and Google in AngularJS app" as being ones that had useful information.

I can't give you a comprehensive answer since I don't remember everything I had to do, nor did I even understand everything I was doing at the time, but I can give you the general idea. You are on the right track.

Essentially I ended up using the token granted by Facebook to confirm that they were logged into their Facebook account, created a user based on their Facebook user ID, and granted them my own bearer token that they could use to access my API.

The flow looks something like this:

  1. Client authenticates with Facebook via whatever method (we used oauth.io)
    • Facebook returns them a token
  2. Client sends token information to the registration endpoint of my WebApi controller
    • The token is validated using Facebook's Graph API, which returns user info
    • A user is created in the database via ASP.NET Identity with their Facebook user ID as the key
  3. Client sends token information to the authentication endpoint of my WebApi controller
    • The token is validated using Facebook's Graph API, which returns user info
    • The user info is used to look up the user in the database, confirm they have previously registered
    • ASP.NET Identity is used to generate a new token for that user
    • That token is returned to the client
  4. Client includes an Authorization header in all future HTTP requests with the new token granted by my service (ex. "Authorization: Bearer TOKEN")
    • If the WebApi endpoint has the [Authorize] attribute, ASP.NET Identity will automatically validate the bearer token and refuse access if it is not valid

There ended up being a lot of custom code for implementing the OAuth stuff with ASP.NET Identity, and those links you included show you some of that. Hopefully this information will help you a little bit, sorry I couldn't help more.

like image 123
mattherman Avatar answered Oct 13 '22 23:10

mattherman


I followed this article. The flow is basically this

  • The server has the facebook keys just like with web login
  • The app asks for available social logins and displays buttons (you can hardcode this I guess)
  • When a button is pressed the app opens a browser and sets the URL to the one related to the specified social login. The ASP.NET then redirects the browser to facebook/google/whatever with the appropriate Challenge
  • The user might be logged in or not and might have given permission to your app or not. After he gives the permissions facebook redirects back to the provided callback URL
  • At that point you can get the external login info from the SignInManager and check if the user already exists and if you should create a new account
  • Finally a token is generated and the browser is redirected to a URL in which the token is placed. The app gets the token from the URL and closes the browser. Uses the token to proceed with API requests.

Honestly I have no idea if this approach is legit...

The code of the action buttons should redirect to:

public async Task<IEnumerable<ExternalLoginDto>> GetExternalLogins(string returnUrl, bool generateState = false)
{
    IEnumerable<AuthenticationScheme> loginProviders = await SignInManager.GetExternalAuthenticationSchemesAsync();
    var logins = new List<ExternalLoginDto>();

    string state;

    if (generateState)
    {
        const int strengthInBits = 256;
        state = RandomOAuthStateGenerator.Generate(strengthInBits);
    }
    else
    {
        state = null;
    }

    foreach (AuthenticationScheme authenticationScheme in loginProviders)
    {
        var routeValues = new
        {
            provider = authenticationScheme.Name,
            response_type = "token",
            client_id = Configuration["Jwt:Issuer"],
            redirect_uri = $"{Request.Scheme}//{Request.Host}{returnUrl}",
            state = state
        };

        var login = new ExternalLoginDto
        {
            Name = authenticationScheme.DisplayName,
            Url = Url.RouteUrl("ExternalLogin", routeValues),
            State = state
        };

        logins.Add(login);
    }

    return logins;
}

The code for the callback action:

[Authorize(AuthenticationSchemes = "Identity.External")]
[Route("ExternalLogin", Name = "ExternalLogin")]
public async Task<IActionResult> GetExternalLogin(string provider, string state = null, string client_id = null, string error = null)
{
    if (error != null)
    {
        ThrowBadRequest(error);
    }

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

    string providerKey = User.FindFirstValue(ClaimTypes.NameIdentifier);

    var externalLoginInfo = new ExternalLoginInfo(User, User.Identity.AuthenticationType, providerKey, User.Identity.AuthenticationType);

    if (externalLoginInfo.LoginProvider != provider)
    {
        await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
        return new ChallengeResult(provider);
    }

    var userLoginInfo = new UserLoginInfo(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey, externalLoginInfo.ProviderDisplayName);
    User user = await UserManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey);

    if (client_id != Configuration["Jwt:Issuer"])
    {
        return Redirect($"/#error=invalid_client_id_{client_id}");
    }

    if (user != null)
    {
        return await LoginWithLocalUser(user, state);
    }
    else
    {
        string email = null;
        string firstName = null;
        string lastName = null;

        IEnumerable<Claim> claims = externalLoginInfo.Principal.Claims;
        if (externalLoginInfo.LoginProvider == "Google")
        {
            email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
            firstName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
            lastName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
        }
        else if (externalLoginInfo.LoginProvider == "Facebook")
        {
            email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;

            string[] nameParts = claims.First(c => c.Type == ClaimTypes.Name)?.Value.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            firstName = nameParts?.First();
            lastName = nameParts?.Last();
        }

        //some fallback just in case
        firstName ??= externalLoginInfo.Principal.Identity.Name;
        lastName ??= externalLoginInfo.Principal.Identity.Name;

        user = new User
        {
            UserName = email,
            Email = email,
            FirstName = firstName,
            LastName = lastName,
            EmailConfirmed = true //if the user logs in with Facebook consider the e-mail confirmed
        };

        IdentityResult userCreationResult = await UserManager.CreateAsync(user);
        if (userCreationResult.Succeeded)
        {
            userCreationResult = await UserManager.AddLoginAsync(user, userLoginInfo);
            if (userCreationResult.Succeeded)
            {
                return await LoginWithLocalUser(user, state);
            }
        }

        string identityErrrors = String.Join(" ", userCreationResult.Errors.Select(ie => ie.Description));
        Logger.LogWarning($"Error registering user with external login. Email:{email}, Errors:" + Environment.NewLine + identityErrrors);
        return Redirect($"/#error={identityErrrors}");
    }
}

private async Task<RedirectResult> LoginWithLocalUser(User user, string state)
{
    await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

    DateTime expirationDate = DateTime.UtcNow.AddDays(365);

    string token = user.GenerateJwtToken(Configuration["Jwt:Key"], Configuration["Jwt:Issuer"], expirationDate);
    return Redirect($"/#access_token={token}&token_type=bearer&expires_in={(int)(expirationDate - DateTime.UtcNow).TotalSeconds}&state={state}");
}
like image 1
Stilgar Avatar answered Oct 13 '22 23:10

Stilgar