Skip to content
Permalink
Browse files

Add API authentication handler

  • Loading branch information...
tarwn committed Apr 7, 2018
1 parent 1404bde commit 60a3a47a03cec8eda5a99cd82ec87b6601e2507c
@@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace SampleCosmosCore2App.Controllers
{
[Route("api/[controller]")]
[Authorize(Policy = "APIAccessOnly")]
public class ValuesController : Controller
{
// GET api/values
@@ -136,6 +136,8 @@ public async Task<LoginResult> LoginAsync(string userName, string password)
return LoginResult.GetFailed();
}

// add validation that user is allowed to login

// add in option of for multifactor and use options to provide redirect url

await SignInAsync(user);
@@ -152,13 +154,43 @@ public async Task<LoginResult> LoginExternalAsync(string scheme, string identity
return LoginResult.GetFailed();
}

// add validation that user is allowed to login

// add in option of for multifactor and use options to provide redirect url

await SignInAsync(user);

return LoginResult.GetSuccess();
}

public async Task<ClaimsPrincipal> GetOneTimeLoginAsync(string scheme, string userAuthId, string identity, string authenticationScheme)
{
var authScheme = StringToScheme(scheme);
var userAuth = await _persistence.Users.GetUserAuthenticationAsync(userAuthId);

// are the passed auth details valid?
if (userAuth == null)
{
return null;
}

if (userAuth.Scheme != authScheme || !userAuth.Identity.Equals(identity, StringComparison.CurrentCultureIgnoreCase))
{
return null;
}

// is the user allowed to log in?
var user = await _persistence.Users.GetUserAsync(userAuth.UserId);

// add validation that user is allowed to login

// create a claims principal
var claimsIdentity = new ClaimsIdentity(authenticationScheme);
claimsIdentity.AddClaim(new Claim("userId", userAuth.UserId));
claimsIdentity.AddClaim(new Claim("userAuthId", userAuth.Id));
return new ClaimsPrincipal(claimsIdentity);
}

private async Task SignInAsync(LoginUser user)
{
// key the login to a server-side session id to make it easy to invalidate later
@@ -170,6 +202,7 @@ private async Task SignInAsync(LoginUser user)
session = await _persistence.Users.CreateSessionAsync(session);

var identity = new ClaimsIdentity(Options.AuthenticationType);
identity.AddClaim(new Claim("userId", session.UserId));
identity.AddClaim(new Claim("sessionId", session.Id));
await _context.HttpContext.SignInAsync(new ClaimsPrincipal(identity));
}
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using SampleCosmosCore2App.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace SampleCosmosCore2App.Membership
{
public class CustomMembershipAPIAuthHandler : AuthenticationHandler<CustomMembershipAPIOptions>
{
private ICustomMembership _membership;

public CustomMembershipAPIAuthHandler(ICustomMembership membership, IOptionsMonitor<CustomMembershipAPIOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
_membership = membership;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Is this relevant to us?
if (!Request.Headers.TryGetValue(HeaderNames.Authorization, out var authorization))
{
return AuthenticateResult.NoResult();
}

var actualAuthValue = authorization.FirstOrDefault(s => s.StartsWith(Options.Scheme, StringComparison.CurrentCultureIgnoreCase));
if (actualAuthValue == null)
{
return AuthenticateResult.NoResult();
}

// Is it a good pair?
var apiPair = actualAuthValue.Substring(Options.Scheme.Length + 1);
var apiValues = apiPair.Split(':', 2);
if (apiValues.Length != 2 || String.IsNullOrEmpty(apiValues[0]) || String.IsNullOrEmpty(apiValues[1]))
{
return AuthenticateResult.Fail($"Invalid authentication format, expected '{Options.Scheme} id:secret'");
}

var principal = await _membership.GetOneTimeLoginAsync("APIKey", apiValues[0], apiValues[1], Options.Scheme);
if (principal == null)
{
return AuthenticateResult.Fail("Invalid authentication provided, access denied.");
}

var ticket = new AuthenticationTicket(principal, Options.Scheme);
return AuthenticateResult.Success(ticket);
}

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = $"Basic realm=\"{Options.Realm}\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}

protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
{
return base.HandleForbiddenAsync(properties);
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SampleCosmosCore2App.Membership
{
public class CustomMembershipAPIOptions : AuthenticationSchemeOptions
{
private static string DefaultScheme = "CustomMembershipAPIKey";

public CustomMembershipAPIOptions()
{
Scheme = DefaultScheme;
}

public string Scheme { get; set; }
public string Realm { get; set; }

}
}
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -20,5 +21,13 @@ public static IServiceCollection AddCustomMembership<T>(this IServiceCollection

return services;
}

public static AuthenticationBuilder AddCustomMembershipAPIAuth(this AuthenticationBuilder builder, string scheme, string realm)
{
return builder.AddScheme<CustomMembershipAPIOptions, CustomMembershipAPIAuthHandler>(scheme, (options) => {
options.Scheme = scheme;
options.Realm = realm;
});
}
}
}
@@ -18,6 +18,7 @@ public interface ICustomMembership

Task<LoginResult> LoginAsync(string username, string password);
Task<LoginResult> LoginExternalAsync(string scheme, string identity);
Task<ClaimsPrincipal> GetOneTimeLoginAsync(string scheme, string userAuthId, string identity, string authenticationScheme);

Task<bool> ValidateLoginAsync(ClaimsPrincipal principal);

@@ -50,6 +50,8 @@ public void ConfigureServices(IServiceCollection services)
});

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
/* Custom Membership API Provider */
.AddCustomMembershipAPIAuth("APIToken", "SampleCosmosCore2App")
/* External Auth Providers */
.AddCookie("ExternalCookie")
.AddTwitter("Twitter", options =>
@@ -78,6 +80,13 @@ public void ConfigureServices(IServiceCollection services)
};
});

services.AddAuthorization(options => {
options.AddPolicy("APIAccessOnly", policy =>
{
policy.AddAuthenticationSchemes("APIToken");
policy.RequireAuthenticatedUser();
});
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

0 comments on commit 60a3a47

Please sign in to comment.
You can’t perform that action at this time.