|
|
|
@ -0,0 +1,148 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Security.Claims;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Ombi.Models;
|
|
|
|
|
|
|
|
|
|
namespace Ombi.Auth
|
|
|
|
|
{
|
|
|
|
|
public class TokenProviderMiddleware
|
|
|
|
|
{
|
|
|
|
|
private readonly RequestDelegate _next;
|
|
|
|
|
private readonly TokenProviderOptions _options;
|
|
|
|
|
private readonly JsonSerializerSettings _serializerSettings;
|
|
|
|
|
|
|
|
|
|
public TokenProviderMiddleware(
|
|
|
|
|
RequestDelegate next,
|
|
|
|
|
IOptions<TokenProviderOptions> options)
|
|
|
|
|
{
|
|
|
|
|
_next = next;
|
|
|
|
|
|
|
|
|
|
_options = options.Value;
|
|
|
|
|
ThrowIfInvalidOptions(_options);
|
|
|
|
|
|
|
|
|
|
_serializerSettings = new JsonSerializerSettings
|
|
|
|
|
{
|
|
|
|
|
Formatting = Formatting.Indented
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task Invoke(HttpContext context)
|
|
|
|
|
{
|
|
|
|
|
// If the request path doesn't match, skip
|
|
|
|
|
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
return _next(context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request must be POST with Content-Type: application/json
|
|
|
|
|
if (!context.Request.Method.Equals("POST")
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
context.Response.StatusCode = 400;
|
|
|
|
|
return context.Response.WriteAsync("Bad request.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return GenerateToken(context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task GenerateToken(HttpContext context)
|
|
|
|
|
{
|
|
|
|
|
var request = context.Request;
|
|
|
|
|
UserAuthModel userInfo; // TODO use a stong type
|
|
|
|
|
|
|
|
|
|
using (var bodyReader = new StreamReader(request.Body))
|
|
|
|
|
{
|
|
|
|
|
string body = await bodyReader.ReadToEndAsync();
|
|
|
|
|
userInfo = JsonConvert.DeserializeObject<UserAuthModel>(body);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var identity = await _options.IdentityResolver(userInfo.Username, userInfo.Password);
|
|
|
|
|
if (identity == null)
|
|
|
|
|
{
|
|
|
|
|
context.Response.StatusCode = 400;
|
|
|
|
|
await context.Response.WriteAsync("Invalid username or password.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
// Specifically add the jti (nonce), iat (issued timestamp), and sub (subject/user) claims.
|
|
|
|
|
// You can add other claims here, if you want:
|
|
|
|
|
var jwtClaims = new List<Claim>
|
|
|
|
|
{
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.Sub, userInfo.Username),
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.Jti, await _options.NonceGenerator()),
|
|
|
|
|
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUniversalTime().ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
identity.Claims.ToList().AddRange(jwtClaims);
|
|
|
|
|
|
|
|
|
|
// Create the JWT and write it to a string
|
|
|
|
|
var jwt = new JwtSecurityToken(
|
|
|
|
|
issuer: _options.Issuer,
|
|
|
|
|
audience: _options.Audience,
|
|
|
|
|
claims: identity.Claims,
|
|
|
|
|
notBefore: now,
|
|
|
|
|
expires: now.Add(_options.Expiration),
|
|
|
|
|
signingCredentials: _options.SigningCredentials);
|
|
|
|
|
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
|
|
|
|
|
|
|
|
|
|
var response = new
|
|
|
|
|
{
|
|
|
|
|
access_token = encodedJwt,
|
|
|
|
|
expires_in = (int)_options.Expiration.TotalSeconds
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Serialize and return the response
|
|
|
|
|
context.Response.ContentType = "application/json";
|
|
|
|
|
await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ThrowIfInvalidOptions(TokenProviderOptions options)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(options.Path))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.Path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(options.Issuer))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.Issuer));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(options.Audience))
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.Audience));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.Expiration == TimeSpan.Zero)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(TokenProviderOptions.Expiration));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.IdentityResolver == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.IdentityResolver));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.SigningCredentials == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.SigningCredentials));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.NonceGenerator == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(TokenProviderOptions.NonceGenerator));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|