Created
April 5, 2019 10:46
-
-
Save jmcd/07ce68e15f02270eeb0c19157430b02c to your computer and use it in GitHub Desktop.
Get external (eg social) claims while logged in with another identity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?}"); | |
}); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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