ASP.NET Web API social authentication for Web and Mobile - c#

My question is kind of complex so bear with me as I try to lay it out nicely what I am struggling with.
Goal
Have an ASP.NET website that lets users register & sign-in via Username/Password or Social (Facebook, Twitter, Google, etc) that also has an API. This API needs to be locked down with [Authorize]. The API needs to be able to be accessed by mobile clients (Android, iOS, etc) that can be signed in via Username/Password or Social (Facebook, Twitter, Google, etc).
Background
So I have done sites that can do one or two things from my goal but not all together. There are great examples online and built in examples into VS projects that show how to let the user register and sign-in via social apps but they are only for the website and not for mobile. I have done a website that an Android app uses Username/Password to authenticate with that API, but nothing with OAuth or Social credentials.
I started out using this page as a reference but I have no clue how to take that and make it work for my website logging in and for my mobile app logging in.
This guy makes it sound so easy but doesn't show any code for this.
Question
Is there a tutorial or GitHub example somewhere that can get me to my goal? I basically want a website where people can register a username/password or use their social account AND also let the user do the same (register & login) via a mobile device. The mobile device will basically just use the API to push/pull data, but I am unsure how to incorporate social logins with my API. I assume I need to use OAuth and go that route but I cannot find any good examples that show how to do this for both web and mobile.
Or maybe is the right solution is to have the webpage be all cookie auth and the API be a separate "web site" and be all token auth and they both tie to the same database?

I've successfully done this very task within my own ASP.NET MVC application using ASP.NET Identity, but then hit the issue you mention: I need this to work using Web API as well so that my mobile app can interact natively.
I was unfamiliar with the article you linked, but after reading through it, I noticed that a lot of the work and code their is not necessary and complicates functionality that already exists within ASP.NET Identity.
Here are my recommendations, and I am assuming you are using ASP.NET Identity V2 which is equivalent to the packages surrounding MVC5 (not the new MVC6 vNext). This will allow both your website AND mobile application via API to authenticate both with a local login (username/password) and an external OAuth provider both from MVC web views on your website and through Web API calls from your mobile application:
Step 1. When creating your project, insure you have both the required packages for MVC and Web API included. In the ASP.NET Project Selection dialog you will have the option to select the checkboxes, insure MVC and Web API are both checked. If you didn't already do this when you created your project, I would recommend creating a new project and migrating your existing code over versus searching and manually adding the dependencies and template code.
Step 2. Inside your Startup.Auth.cs file, you will need code to tell OWIN to use cookie authentication, allow external sign in cookies, and support OAuth bearer tokens (This is how Web API calls will authenticate). These are relevant excerpts from my working project codebase:
Startup.Auth.cs
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/account/externallogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
//AllowInsecureHttp = false
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
app.UseTwitterAuthentication(
consumerKey: "Twitter API Key",
consumerSecret: "Twitter API Secret");
app.UseFacebookAuthentication(
appId: "Facebook AppId",
appSecret: "Facebook AppSecret");
In the above code I currently support Twitter and Facebook as external authentication providers; however, you can add additional external providers with the app.UserXYZProvider calls and additional libraries and they will plug and play with the code I provide here.
Step 3. Inside your WebApiConfig.cs file, you must configure the HttpConfiguration to supress default host authentication and support OAuth bearer tokens. To explain, this tells your application to differentiate authentication types between MVC and Web API, this way you can use the typical cookie flow for the website, meanwhile your application will accept bearer tokens in the form of OAuth from the Web API without complaining or other issues.
WebApiConfig.cs
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Step 4. You need an AccountController (or equivalently purposed controller) for both MVC and Web API. In my project I have two AccountController files, one MVC controller inheriting from the base Controller class, and another AccountController inheriting from ApiController that is in a Controllers.API namespace to keep things clean. I am using the standard template AccountController code from the Web API and MVC projects. Here is the API version of the Account Controller:
AccountController.cs (Controllers.API namespace)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Disco.Models.API;
using Disco.Providers;
using Disco.Results;
using Schloss.AspNet.Identity.Neo4j;
using Disco.Results.API;
namespace Disco.Controllers.API
{
[Authorize]
[RoutePrefix("api/account")]
public class AccountController : ApiController
{
private const string LocalLoginProvider = "Local";
private ApplicationUserManager _userManager;
public AccountController()
{
}
public AccountController(ApplicationUserManager userManager,
ISecureDataFormat<AuthenticationTicket> accessTokenFormat)
{
UserManager = userManager;
AccessTokenFormat = accessTokenFormat;
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? Request.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }
// GET account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("userinfo")]
public UserInfoViewModel GetUserInfo()
{
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null ? externalLogin.LoginProvider : null
};
}
// POST account/Logout
[Route("logout")]
public IHttpActionResult Logout()
{
Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return Ok();
}
// GET account/ManageInfo?returnUrl=%2F&generateState=true
[Route("manageinfo")]
public async Task<ManageInfoViewModel> GetManageInfo(string returnUrl, bool generateState = false)
{
IdentityUser user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null)
{
return null;
}
List<UserLoginInfoViewModel> logins = new List<UserLoginInfoViewModel>();
foreach (UserLoginInfo linkedAccount in await UserManager.GetLoginsAsync(User.Identity.GetUserId()))
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = linkedAccount.LoginProvider,
ProviderKey = linkedAccount.ProviderKey
});
}
if (user.PasswordHash != null)
{
logins.Add(new UserLoginInfoViewModel
{
LoginProvider = LocalLoginProvider,
ProviderKey = user.UserName,
});
}
return new ManageInfoViewModel
{
LocalLoginProvider = LocalLoginProvider,
Email = user.UserName,
Logins = logins,
ExternalLoginProviders = GetExternalLogins(returnUrl, generateState)
};
}
// POST account/ChangePassword
[Route("changepassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword,
model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/SetPassword
[Route("setpassword")]
public async Task<IHttpActionResult> SetPassword(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await UserManager.AddPasswordAsync(User.Identity.GetUserId(), model.NewPassword);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/AddExternalLogin
[Route("addexternallogin")]
public async Task<IHttpActionResult> AddExternalLogin(AddExternalLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
AuthenticationTicket ticket = AccessTokenFormat.Unprotect(model.ExternalAccessToken);
if (ticket == null || ticket.Identity == null || (ticket.Properties != null
&& ticket.Properties.ExpiresUtc.HasValue
&& ticket.Properties.ExpiresUtc.Value < DateTimeOffset.UtcNow))
{
return BadRequest("External login failure.");
}
ExternalLoginData externalData = ExternalLoginData.FromIdentity(ticket.Identity);
if (externalData == null)
{
return BadRequest("The external login is already associated with an account.");
}
IdentityResult result = await UserManager.AddLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(externalData.LoginProvider, externalData.ProviderKey));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RemoveLogin
[Route("removelogin")]
public async Task<IHttpActionResult> RemoveLogin(RemoveLoginBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result;
if (model.LoginProvider == LocalLoginProvider)
{
result = await UserManager.RemovePasswordAsync(User.Identity.GetUserId());
}
else
{
result = await UserManager.RemoveLoginAsync(User.Identity.GetUserId(),
new UserLoginInfo(model.LoginProvider, model.ProviderKey));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// GET account/ExternalLogin
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)]
[AllowAnonymous]
[Route("externallogin", Name = "ExternalLoginAPI")]
public async Task<IHttpActionResult> 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);
}
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
if (externalLogin == null)
{
return InternalServerError();
}
if (externalLogin.LoginProvider != provider)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
return new ChallengeResult(provider, this);
}
ApplicationUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider,
externalLogin.ProviderKey));
bool hasRegistered = user != null;
if (hasRegistered)
{
Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
}
else
{
IEnumerable<Claim> claims = externalLogin.GetClaims();
ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
Authentication.SignIn(identity);
}
return Ok();
}
// GET account/ExternalLogins?returnUrl=%2F&generateState=true
[AllowAnonymous]
[Route("externallogins")]
public IEnumerable<ExternalLoginViewModel> GetExternalLogins(string returnUrl, bool generateState = false)
{
IEnumerable<AuthenticationDescription> descriptions = Authentication.GetExternalAuthenticationTypes();
List<ExternalLoginViewModel> logins = new List<ExternalLoginViewModel>();
string state;
if (generateState)
{
const int strengthInBits = 256;
state = RandomOAuthStateGenerator.Generate(strengthInBits);
}
else
{
state = null;
}
foreach (AuthenticationDescription description in descriptions)
{
ExternalLoginViewModel login = new ExternalLoginViewModel
{
Name = description.Caption,
Url = Url.Route("ExternalLogin", new
{
provider = description.AuthenticationType,
response_type = "token",
client_id = Startup.PublicClientId,
redirect_uri = new Uri(Request.RequestUri, returnUrl).AbsoluteUri,
state = state
}),
State = state
};
logins.Add(login);
}
return logins;
}
// POST account/Register
[AllowAnonymous]
[Route("register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user, model.Password);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
// POST account/RegisterExternal
[OverrideAuthentication]
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("registerexternal")]
public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var info = await Authentication.GetExternalLoginInfoAsync();
if (info == null)
{
return InternalServerError();
}
var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult result = await UserManager.CreateAsync(user);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (!result.Succeeded)
{
return GetErrorResult(result);
}
return Ok();
}
protected override void Dispose(bool disposing)
{
if (disposing && _userManager != null)
{
_userManager.Dispose();
_userManager = null;
}
base.Dispose(disposing);
}
#region Helpers
private IAuthenticationManager Authentication
{
get { return Request.GetOwinContext().Authentication; }
}
private IHttpActionResult GetErrorResult(IdentityResult result)
{
if (result == null)
{
return InternalServerError();
}
if (!result.Succeeded)
{
if (result.Errors != null)
{
foreach (string error in result.Errors)
{
ModelState.AddModelError("", error);
}
}
if (ModelState.IsValid)
{
// No ModelState errors are available to send, so just return an empty BadRequest.
return BadRequest();
}
return BadRequest(ModelState);
}
return null;
}
private class ExternalLoginData
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string UserName { get; set; }
public IList<Claim> GetClaims()
{
IList<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, ProviderKey, null, LoginProvider));
if (UserName != null)
{
claims.Add(new Claim(ClaimTypes.Name, UserName, null, LoginProvider));
}
return claims;
}
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer)
|| String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name)
};
}
}
private static class RandomOAuthStateGenerator
{
private static RandomNumberGenerator _random = new RNGCryptoServiceProvider();
public static string Generate(int strengthInBits)
{
const int bitsPerByte = 8;
if (strengthInBits % bitsPerByte != 0)
{
throw new ArgumentException("strengthInBits must be evenly divisible by 8.", "strengthInBits");
}
int strengthInBytes = strengthInBits / bitsPerByte;
byte[] data = new byte[strengthInBytes];
_random.GetBytes(data);
return HttpServerUtility.UrlTokenEncode(data);
}
}
#endregion
}
}
Step 5. You also need to create an ApplicationOAuthProvider so the server can generate and validate OAuth tokens. This is provided in the WebAPI sample project. This is my version of the file:
ApplicationOAuthProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Butler.Models;
using Schloss.AspNet.Identity.Neo4j;
namespace Butler.Providers
{
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _publicClientId;
public ApplicationOAuthProvider(string publicClientId)
{
if (publicClientId == null)
{
throw new ArgumentNullException("publicClientId");
}
_publicClientId = publicClientId;
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
CookieAuthenticationDefaults.AuthenticationType);
AuthenticationProperties properties = CreateProperties(user.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Resource owner password credentials does not provide a client ID.
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
//Uri expectedRootUri = new Uri(context.Request.Uri, "/");
//if (expectedRootUri.AbsoluteUri == context.RedirectUri)
//{
context.Validated();
//}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
}
Also included is the ChallengeResult, which the Web API arm of your application will use to handle challenges provided by the external login providers to authenticate your user:
ChallengeResult.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
namespace Butler.Results
{
public class ChallengeResult : IHttpActionResult
{
public ChallengeResult(string loginProvider, ApiController controller)
{
LoginProvider = loginProvider;
Request = controller.Request;
}
public string LoginProvider { get; set; }
public HttpRequestMessage Request { get; set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
Request.GetOwinContext().Authentication.Challenge(LoginProvider);
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
response.RequestMessage = Request;
return Task.FromResult(response);
}
}
}
With that set of code, you will be able to HTTP GET and HTTP POST the routes on the API version of the AccountController to register a user, login using username and password to receive a Bearer token, add/remove external logins, manage external logins, and most importantly for your issue, authenticate by passing in an external login token in exchange for an OAuth bearer token for you application.

You may want to have a look at this series of articles to see whether it covers your goal:
Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
by Taiseer Joudeh (who also frequently answers questions on SO)
The articles are about creating a token based authentication service using OWIN and one of the parts cover using external logins (such as Facebook and Google+). The examples are primarily centered around a web application as consumer of the web service, but it should work on mobile applications as well. The articles have a GitHub project associated and a very active comment section, where hardly any question goes unanswered.
Hope this may lead you to your goal.

I am adding this as a seperate answer to the second portion of your question to say that YES you can have two separate projects tied to the same database and simply have the MVC/Web Forms website project use all cookie authentication and then have a separate Web API project that is all token authentication.
In my longer answer with source code examples what I have basically done is combine the two separate projects into one project so as to avoid redundant model code and controller code. In my case this made more sense for me; however, I am inclined to say that it is up to personal preference and the needs of your project to dictate whether to maintain two separate projects, one website and one web API endpoint, or to combine them.
ASP.NET was designed to be very flexible and plug and play as a middleware and I can attest that my project has existed and functioned exactly as intended with the code in two separate projects and now as one combined project.

Related

Google Sign-in with existing ASP.NET MVC Application

I'm trying to handle google-signin to my existing ASP.NET MVC application. It has a forms authentication developped customely and it works fine for username and password.
I want to add Google-sign-in to this project. I currently added the button, created the app in google developper console got my id and secret and set up the urls.
I can see the button in login page, click, i see my account and picture, select account.
At this point google posts me some data. I received in an Action method an string array object called "credential" which has a string in first position. But I don't know what to do from here on...
Can somebody help me with this? Which document I should use?
I'm reading this: https://developers.google.com/identity/gsi/web till now but i'm stuck.
What I dont want is this:
https://learn.microsoft.com/en-us/aspnet/mvc/overview/security/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on
I want to handle the requests needed myself (with cookies, database checks and keeping track of tokens) and get the user information google provides by myself in my controller's action methods.
Here is a part of the razor view code
<div id="g_id_onload"
data-client_id="my id is here"
data-login_uri="#Url.Action("LoginWithGoogle","Login")"
data-auto_prompt="false">
</div>
<div class="g_id_signin"
data-type="standard"
data-size="large"
data-theme="outline"
data-text="sign_in_with"
data-shape="rectangular"
data-logo_alignment="left"
data-auto_prompt="true"
>
</div>
I added this script:
<script src="https://accounts.google.com/gsi/client" async defer></script>
This Action method can catch the string:
[HttpPost]
public ActionResult LoginWithGoogle(string[] credential)
{
ViewBag.Data = credential;
///I will do necessary stuff here.
return View();
}
Notes:
-Identity is not installed and will not be used (if unless impossible without using it).
-My .Net Framework version: 4.7.2
Thanks for the help.
I don't use "full" default auth code too, here is how I handle Google/Yandex/Discord/OAuth response:
[HttpPost]
[AllowAnonymous]
public IActionResult ExternalLogin(string provider, string? returnUrl = null)
{
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), null, new { returnUrl });
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null)
{
var info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return this.Redirect(LoginPath);
}
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
var name = info.Principal.FindFirstValue(ClaimTypes.Name) ?? info.Principal.Identity.Name;
// your own actions & checks with email, name etc
This still required some "default" preparations in Startup:
services.AddIdentity<User, UserStoreService.UserRole>()
.AddUserStore<UserStoreService>()
.AddRoleStore<UserStoreService>()
.AddDefaultTokenProviders();
services.AddAuthentication().AddGoogle(...)
But here User is my own class, UserRole is own (end empty) class, and UserStoreService is my own implementation of IDisposable, IUserStore<User>, IUserEmailStore<User>, IUserClaimStore<User>, IUserSecurityStampStore<User>, IRoleStore<UserStoreService.UserRole> (you may modify this list according to your needs)
This is example how to process external authentication using owin. First of all, you have to setup your startup.cs and update configuration with something like this. Of course you have to import required packages into your project.
public void Configuration(IAppBuilder app)
{
....
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "YourGoogleClientId",
ClientSecret = "YourGoogleClientSecret",
});
}
Then in your account controller (example name) create method that will handle your login with google button. For example:
public ActionResult ExternalLogin(string provider)
{
var returnUrl = Request.Url.GetLeftPart(UriPartial.Authority) + "/Account/ExternalLoginCallback";
return new ChallengeResult(provider, returnUrl);
}
Create your ChallengeResult method:
internal class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUrl)
{
LoginProvider = provider;
RedirectUri = redirectUrl;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
var owin = context.HttpContext.GetOwinContext();
owin.Authentication.Challenge(properties, LoginProvider);
}
}
Add your new authentication method in your account controller. For example:
public async Task<ActionResult> ExternalLoginCallback()
{
try
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null || string.IsNullOrEmpty(loginInfo.Email))
{
return RedirectToAction("ExternalLoginFailed", "WebAccount");
}
else
{
// handle external login, means process user login in your system
}
}
catch (Exception ex)
{
// handle possible exceptions
}
return RedirectToAction("ExternalLoginFailed", "WebAccount");
}
private IAuthenticationManager AuthenticationManager
{
get { return HttpContext.GetOwinContext().Authentication; }
}

Basic Token authentification and authorization on Web.Api

So I have an MVC Application that calls WebApi method.
My Authorization on MVC App is done like this
public class CustomAuthorizeAttribute : AuthorizeAttribute {
private RolesEnum _role;
public CustomAuthorizeAttribute() {
_role = RolesEnum.User;
}
public CustomAuthorizeAttribute(RolesEnum role) {
_role = role;
}
protected override bool AuthorizeCore(HttpContextBase httpContext) {
User currentUser = (User)httpContext.Session["CurrentUser"];
if (currentUser == null) {
return false;
}
if (currentUser.Role == RolesEnum.User && _role == RolesEnum.Admin) {
return false;
}
return true;
}
The authentification is done calling a WebApi method
[HttpPost]
public ActionResult Login(string username, string password)
{
User acc = new User();
acc.Username = username;
acc.Password = password;
acc = accBL.Login(acc);
if (acc != null) {
Session.Add("CurrentUser", acc);
return RedirectToAction("Index", "Project", null);
} else {
return View();
}
}
Login method looks like this
public User LogIn(User acc) {
try {
HttpClient client = new HttpClient();
client.BaseAddress = new Uri(BASE_URL);
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = client.PostAsJsonAsync("api/Account/Login", acc).Result;
if (response.IsSuccessStatusCode) {
return response.Content.ReadAsAsync<User>().Result;
} else {
return null;
}
} catch {
return null;
}
}
And WebApi method looks like this
[Route("api/Account/Login")]
[HttpPost]
public IHttpActionResult Login(User userModel) {
User user = db.Users.Where(p => p.Username == userModel.Username && p.Password == userModel.Password).FirstOrDefault();
if (user != null) {
return Ok(user);
} else {
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
}
How can I make a connection between the MVC App and the WebApi Services. My authorization and authentification works on MVC part, but my WebApi Services can be called without this any authorization/authenification. How can I secure my WebApi too based on my example? I've been working for about 3 weeks with MVC and WebApi and many things are not very clear too me.
Should I just create a GUID in public IHttpActionResult Login(User userModel) and check for it everytime i a method is called? How do i pass this GUID to MVC App and from MVC to WebApi?
What you can do is create some sort of a token (eg. JWT) in the WebAPI Login() method and return with the Ok() response (to the MVC app). Users calling your API endpoints have to send this token back (eg. in a custom "Token" header). You can validate the token inside a custom WebAPI authorize attribute that you use in your API endpoints.
eg.
Login endpoint
[Route("api/Account/Login")]
[HttpPost]
public object Login(User userModel) {
User user = ...;
string token = CreateTokenForUser(user);
if (user != null) {
// return user and token back
return new {User = user, Token = token};
} else {
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
}
Custom authentication filter
public class UserAuthAttribute : ActionFilterAttribute, IAuthenticationFilter
{
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
string token = null;
IEnumerable<string> tokenHeader;
if (context.Request.Headers.TryGetValues("Token", out tokenHeader))
token = tokenHeader.FirstOrDefault();
if (token != null && IsTokenValid(token)
{
// user has been authenticated i.e. send us a token we sent back earlier
}
else
{
// set ErrorResult - this will result in sending a 401 Unauthorized
context.ErrorResult = new AuthenticationFailureResult(Invalid token", context.Request);
}
}
}
Other endpoint which only authenticated users should be allowed access
[Route("api/Values")]
[HttpGet]
[UserAuth]
public object GetValues() {
// only requests with a valid token will be able to call this because of the [UserAuth] attribute
}

WebApi 2 OAuth External Login Access Token Issue

I've been following a tutorial on webapi oauth login here;
http://bitoftech.net/2014/08/11/asp-net-web-api-2-external-logins-social-logins-facebook-google-angularjs-app/
It all runs smoothly but I am having difficulty with retrieving the token sent back from the external provider (in this test case Google).
So after the user authenticates and confirms the login the "ExternalLogin" end point for the second time on the webapi with the authentication data.
in this method it calls the following to extract all the data to a class
ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
Its here that it seems to be falling over. As when it call the FromIdentity method;
public static ExternalLoginData FromIdentity(ClaimsIdentity identity)
{
if (identity == null)
{
return null;
}
Claim providerKeyClaim = identity.FindFirst(ClaimTypes.NameIdentifier);
if (providerKeyClaim == null || String.IsNullOrEmpty(providerKeyClaim.Issuer) || String.IsNullOrEmpty(providerKeyClaim.Value))
{
return null;
}
if (providerKeyClaim.Issuer == ClaimsIdentity.DefaultIssuer)
{
return null;
}
return new ExternalLoginData
{
LoginProvider = providerKeyClaim.Issuer,
ProviderKey = providerKeyClaim.Value,
UserName = identity.FindFirstValue(ClaimTypes.Name),
ExternalAccessToken = identity.FindFirstValue("ExternalAccessToken"),
};
}
the line;
ExternalAccessToken = identity.FindFirstValue("ExternalAccessToken")
is returning as null? I can't see this token being returned in any of the claims?
The ExternalAccessToken is custom claim added. Please check the following code which is extend from the default providers.
For Google
public class GoogleAuthProvider : IGoogleOAuth2AuthenticationProvider
{
public void ApplyRedirect(GoogleOAuth2ApplyRedirectContext context)
{
context.Response.Redirect(context.RedirectUri);
}
public Task Authenticated(GoogleOAuth2AuthenticatedContext context)
{
context.Identity.AddClaim(new Claim("ExternalAccessToken", context.AccessToken));
return Task.FromResult<object>(null);
}
public Task ReturnEndpoint(GoogleOAuth2ReturnEndpointContext context)
{
return Task.FromResult<object>(null);
}
}
For Facebook
public class FacebookAuthProvider : FacebookAuthenticationProvider
{
public override Task Authenticated(FacebookAuthenticatedContext context)
{
context.Identity.AddClaim(new Claim("ExternalAccessToken", context.AccessToken));
return Task.FromResult<object>(null);
}
}
In these classes added the claim using the following line;
context.Identity.AddClaim(new Claim("ExternalAccessToken", context.AccessToken));

Identity Role-based Authorization is not working

I have a custom implementation of the ASP.NET Identity base, using Dapper instead of Entity Framework largely from the tutorial here: http://blog.markjohnson.io/exorcising-entity-framework-from-asp-net-identity/.
Everything is fine with signing users in and out with my AuthenticationManager. However, as soon as I redirect anywhere after logging the user in, the httpcontext is basically null and the user is no longer authenticated. If I use the [Authorize] attribute as well, then the user is automatically declared as Unauthorized, throwing a 401 error.
Here are parts of my AccountController:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(Login login, string redundant)
{
var master = new MasterModel();
if (ModelState.IsValid && (!string.IsNullOrEmpty(login.Email) && !string.IsNullOrEmpty(login.PasswordHash)))
{
var user = await Models.User.FetchUserByEmail(login.Email);
if (user != null)
{
await SignInAsync(user, true);
master.User = user; // User is now signed in - No problem
return RedirectToAction("Overview", "Account", master);
}
}
TempData["Message"] = "Your username or password was not recognised. Please try again.";
return View(master);
}
[HttpGet]
//[Authorize(Roles = "admin,subscriber")] // 403 when uncommented
public ActionResult Overview(MasterModel master = null)
{
// master is just a blank new MasterModel ??
if (!HttpContext.User.Identity.IsAuthenticated)
{
// User is always null/blank identity
TempData["Message"] = "Please log in to view this content";
return RedirectToAction("Login", "Account", master);
}
var userName = string.IsNullOrEmpty(HttpContext.User.Identity.Name)
? TempData["UserName"].ToString()
: HttpContext.User.Identity.Name;
var user = Models.User.FetchUserByEmail(userName).Result;
if (master == null) master = new MasterModel();
master.User = user;
return View(master);
}
My UserStore implements the following interfaces:
public class UserStore : IUserStore<User>, IUserPasswordStore<User>, IUserSecurityStampStore<User>, IQueryableUserStore<User>, IUserRoleStore<User>
My RoleStore just implements IQueryableRoleStore<Role>
User and Role simply implement IUser and IRole respectively
What am I missing?
Update1:
Here's part of the AuthenticatonManager:
public IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private async Task SignInAsync(User user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Thanks to #WiktorZychla for pointing out the answer.
Turns out I was missing a fundamental step of adding the cookie authentication to IAppBuilder.
Here's how the OwinStartup.cs now looks for reference:
using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
[assembly: OwinStartup(typeof(appNamespace.OwinStartup))]
namespace appNamespace
{
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
}
}
}
Hopefully this will save someone else from tearing their hair out!

How to use Api key in Web Api for service authentication using forms authentication

I am using MVC 4 Web Api and I want the users to be authenticated, before using my service.
I have implemented an authorization message handler, that works just fine.
public class AuthorizationHandler : DelegatingHandler
{
private readonly AuthenticationService _authenticationService = new AuthenticationService();
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
IEnumerable<string> apiKeyHeaderValues = null;
if (request.Headers.TryGetValues("X-ApiKey", out apiKeyHeaderValues))
{
var apiKeyHeaderValue = apiKeyHeaderValues.First();
// ... your authentication logic here ...
var user = _authenticationService.GetUserByKey(new Guid(apiKeyHeaderValue));
if (user != null)
{
var userId = user.Id;
var userIdClaim = new Claim(ClaimTypes.SerialNumber, userId.ToString());
var identity = new ClaimsIdentity(new[] { userIdClaim }, "ApiKey");
var principal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = principal;
}
}
return base.SendAsync(request, cancellationToken);
}
}
The problem is, that I use forms authentication.
[HttpPost]
public ActionResult Login(UserModel model)
{
if (ModelState.IsValid)
{
var user = _authenticationService.Login(model);
if (user != null)
{
// Add the api key to the HttpResponse???
}
return View(model);
}
return View(model);
}
When I call my api:
[Authorize]
public class TestController : ApiController
{
public string GetLists()
{
return "Weee";
}
}
The handler can not find the X-ApiKey header.
Is there a way to add the user's api key to the http response header and to keep the key there, as long as the user is logged in?
Is there another way to implement this functionality?
I found the following article http://www.asp.net/web-api/overview/working-with-http/http-cookies
Using it I configured my AuthorizationHandler to use cookies:
public class AuthorizationHandler : DelegatingHandler
{
private readonly IAuthenticationService _authenticationService = new AuthenticationService();
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var cookie = request.Headers.GetCookies(Constants.ApiKey).FirstOrDefault();
if (cookie != null)
{
var apiKey = cookie[Constants.ApiKey].Value;
try
{
var guidKey = Guid.Parse(apiKey);
var user = _authenticationService.GetUserByKey(guidKey);
if (user != null)
{
var userIdClaim = new Claim(ClaimTypes.Name, apiKey);
var identity = new ClaimsIdentity(new[] { userIdClaim }, "ApiKey");
var principal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = principal;
}
}
catch (FormatException)
{
}
}
return base.SendAsync(request, cancellationToken);
}
}
I configured my Login action result:
[HttpPost]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
var user = _authenticationService.Login(model);
if (user != null)
{
_cookieHelper.SetCookie(user);
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "Incorrect username or password");
return View(model);
}
return View(model);
}
Inside it I am using the CookieHelper, that I created. It consists of an interface:
public interface ICookieHelper
{
void SetCookie(User user);
void RemoveCookie();
Guid GetUserId();
}
And a class that implements the interface:
public class CookieHelper : ICookieHelper
{
private readonly HttpContextBase _httpContext;
public CookieHelper(HttpContextBase httpContext)
{
_httpContext = httpContext;
}
public void SetCookie(User user)
{
var cookie = new HttpCookie(Constants.ApiKey, user.UserId.ToString())
{
Expires = DateTime.UtcNow.AddDays(1)
};
_httpContext.Response.Cookies.Add(cookie);
}
public void RemoveCookie()
{
var cookie = _httpContext.Response.Cookies[Constants.ApiKey];
if (cookie != null)
{
cookie.Expires = DateTime.UtcNow.AddDays(-1);
_httpContext.Response.Cookies.Add(cookie);
}
}
public Guid GetUserId()
{
var cookie = _httpContext.Request.Cookies[Constants.ApiKey];
if (cookie != null && cookie.Value != null)
{
return Guid.Parse(cookie.Value);
}
return Guid.Empty;
}
}
By having this configuration, now I can use the Authorize attribute for my ApiControllers:
[Authorize]
public class TestController : ApiController
{
public string Get()
{
return String.Empty;
}
}
This means, that if the user is not logged in. He can not access my api and recieves a 401 error. Also I can retrieve the api key, which I use as a user ID, anywhere in my code, which makes it very clean and readable.
I do not think that using cookies is the best solution, as some user may have disabled them in their browser, but at the moment I have not found a better way to do the authorization.
From your code samples it doesn't seem like you're using Web Forms. Might you be using Forms Authentication? Are you using the Membership Provider inside your service to validate user credentials?
You can use the HttpClient class and maybe its property DefaultRequestHeaders or an HttpRequestMessage from the code that will be calling the API to set the headers.
Here there are some examples of HttpClient:
http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-a-net-client

Categories

Resources