Skip to content

Instantly share code, notes, and snippets.

@jmcd
Created April 5, 2019 10:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmcd/07ce68e15f02270eeb0c19157430b02c to your computer and use it in GitHub Desktop.
Save jmcd/07ce68e15f02270eeb0c19157430b02c to your computer and use it in GitHub Desktop.
Get external (eg social) claims while logged in with another identity
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApplication6.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.WebUtilities;
using WebApplication6.Controllers;
using Microsoft.Extensions.Hosting;
namespace WebApplication6
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services
.AddDefaultIdentity<IdentityUser>()
.AddDefaultUI(UIFramework.Bootstrap4)
.AddEntityFrameworkStores<ApplicationDbContext>();
services
.AddSingleton<ExternalClaimsRequestStore>()
.AddSingleton<IHostedService, ExternalClaimsRequestStoreJanitor>();
services.AddAuthentication().AddTwitter(twitterOptions =>
{
twitterOptions.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
twitterOptions.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
twitterOptions.Events.OnTicketReceived += (Microsoft.AspNetCore.Authentication.TicketReceivedContext context) =>
{
var returnUri = new Uri(context.ReturnUri, UriKind.Absolute);
var query = QueryHelpers.ParseQuery(returnUri.Query);
var token = query["token"];
var reg = context.HttpContext.RequestServices.GetRequiredService<ExternalClaimsRequestStore>();
reg.Complete(token, context.Principal.Claims);
return Task.CompletedTask;
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env)
{
app
.UseDeveloperExceptionPage()
.UseDatabaseErrorPage()
.UseHttpsRedirection()
.UseStaticFiles()
.UseCookiePolicy()
.UseAuthentication()
.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Twitter;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace WebApplication6.Controllers
{
public partial class HomeController : Controller
{
private readonly ExternalClaimsRequestStore reqReg;
public HomeController(ExternalClaimsRequestStore connectionRequestRegistry) => this.reqReg = connectionRequestRegistry;
public IActionResult Index() => View();
public IActionResult ChallengeTwitter()
{
var token = reqReg.TokenByAdding();
var redirectUri = Url.Action(nameof(Connect), "Home", new { token }, Request.Scheme);
var properties = new AuthenticationProperties { RedirectUri = redirectUri };
return Challenge(properties, TwitterDefaults.AuthenticationScheme);
}
public IActionResult Connect(string token)
{
var twitterClaims = reqReg.ConsumeClaims(token);
var localClaims = User.Claims;
/*
* Now do something with local and external claims
*/
return RedirectToAction(nameof(Index));
}
}
// A place to correlate a external-challenge with returned claims
public class ExternalClaimsRequestStore : IDisposable
{
private readonly IDictionary<string, Resolution> tokenToResolutions = new Dictionary<string, Resolution>();
private readonly RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
private class Resolution
{
private Resolution(DateTimeOffset date) => Date = date;
public DateTimeOffset Date { get; }
public ICollection<Claim> Claims { get; private set; }
public static Resolution Pending() => new Resolution(DateTimeOffset.UtcNow);
public void Complete(IEnumerable<Claim> claims) => Claims = claims.ToArray();
}
// 1. Note a new challenge, HttpContext.User is local
public string TokenByAdding()
{
var token = rng.ConstructToken();
tokenToResolutions[token] = Resolution.Pending();
return token;
}
// 2. Result of external auth, store the claims, HttpContext.User is not authenticated
public void Complete(string token, IEnumerable<Claim> claims)
{
if (tokenToResolutions.TryGetValue(token, out var res))
{
res.Complete(claims);
}
}
// 3. Get the claims, HttpContext.User is local
public ICollection<Claim> ConsumeClaims(string token)
{
if (tokenToResolutions.TryGetValue(token, out var res))
{
tokenToResolutions.Remove(token);
return res.Claims;
}
return new Claim[0];
}
// Cleanup stale entries
public int CountByEvictingOlderThan(TimeSpan maxAge)
{
var now = DateTimeOffset.UtcNow;
var tokens = tokenToResolutions.Where(kvp => (now - kvp.Value.Date) > maxAge).Select(kvp => kvp.Key).ToList();
lock (tokenToResolutions)
{
tokens.ForEach(t => tokenToResolutions.Remove(t));
}
return tokens.Count;
}
public void Dispose() => rng.Dispose();
}
// Service to evict older than 5 minutes
public class ExternalClaimsRequestStoreJanitor : BackgroundService
{
private static readonly TimeSpan MaxAge = new TimeSpan(0, 5, 0);
private readonly ExternalClaimsRequestStore connectionRequestRegistry;
private readonly ILogger<ExternalClaimsRequestStoreJanitor> logger;
public ExternalClaimsRequestStoreJanitor(
ExternalClaimsRequestStore connectionRequestRegistry,
ILogger<ExternalClaimsRequestStoreJanitor> logger)
{
this.connectionRequestRegistry = connectionRequestRegistry;
this.logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var i = connectionRequestRegistry.CountByEvictingOlderThan(MaxAge);
logger.LogInformation($"{nameof(ExternalClaimsRequestStoreJanitor)} did evict {i} old requests");
await Task.Delay(10000, stoppingToken);
}
}
}
// Extension to get a random string to use as a token
public static class TokenFactory
{
public static string ConstructToken(this RNGCryptoServiceProvider rng)
{
var tokenData = new byte[32];
rng.GetBytes(tokenData);
return Convert.ToBase64String(tokenData);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment