Add support for basic authentication

- Return WWW-Authenticate header in CreateToken
- Support parsing out password from Authentication: basic header
- Prevent users with colons from being created
This commit is contained in:
Jordan Brown
2020-01-09 20:15:16 -05:00
parent bf75dbf896
commit 66d991c7a4
3 changed files with 82 additions and 17 deletions
+42 -16
View File
@@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
namespace Tgstation.Server.Api
{
@@ -23,27 +24,32 @@ namespace Tgstation.Server.Api
/// <summary>
/// The <see cref="ApiVersion"/> header key
/// </summary>
const string ApiVersionHeader = "Api";
/// <summary>
/// The <see cref="Username"/> header key
/// </summary>
const string UsernameHeader = "Username";
public const string ApiVersionHeader = "api";
/// <summary>
/// The <see cref="InstanceId"/> header key
/// </summary>
const string InstanceIdHeader = "Instance";
public const string InstanceIdHeader = "instance";
/// <summary>
/// The JWT authentication header scheme
/// </summary>
const string JwtAuthenticationScheme = "Bearer";
public const string JwtAuthenticationScheme = "bearer";
/// <summary>
/// The password authentication header scheme
/// The JWT authentication header scheme
/// </summary>
const string PasswordAuthenticationScheme = "Password";
public const string BasicAuthenticationScheme = "basic";
/// <summary>
/// The <see cref="Username"/> header key
/// </summary>
const string UsernameHeader = "username";
/// <summary>
/// The basic authentication header scheme
/// </summary>
const string PasswordAuthenticationScheme = "password";
/// <summary>
/// The current <see cref="System.Reflection.AssemblyName"/>
@@ -180,7 +186,9 @@ namespace Tgstation.Server.Api
InstanceId = instanceId;
}
switch (scheme)
#pragma warning disable CA1308 // Normalize strings to uppercase
switch (scheme.ToLowerInvariant())
#pragma warning restore CA1308 // Normalize strings to uppercase
{
case JwtAuthenticationScheme:
Token = parameter;
@@ -197,6 +205,25 @@ namespace Tgstation.Server.Api
if (fail)
throw new InvalidOperationException("Missing Username header!");
break;
case BasicAuthenticationScheme:
string joinedString;
try
{
var base64Bytes = Convert.FromBase64String(parameter);
joinedString = Encoding.UTF8.GetString(base64Bytes);
}
catch
{
throw new InvalidOperationException("Invalid basic Authorization header!");
}
var basicAuthSplits = joinedString.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
if (basicAuthSplits.Length < 2)
throw new InvalidOperationException("Invalid basic Authorization header!");
Username = basicAuthSplits.First();
Password = String.Concat(basicAuthSplits.Skip(1));
break;
default:
throw new InvalidOperationException("Invalid authentication scheme!");
}
@@ -241,14 +268,13 @@ namespace Tgstation.Server.Api
if (IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
else
{
headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
headers.Add(UsernameHeader, Username);
}
headers.Authorization = new AuthenticationHeaderValue(
BasicAuthenticationScheme,
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{Username}:{Password}")));
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
instanceId = instanceId ?? InstanceId;
instanceId ??= InstanceId;
if (instanceId.HasValue)
headers.Add(InstanceIdHeader, instanceId.ToString());
}
@@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Linq;
using System.Net.Mime;
@@ -118,7 +121,22 @@ namespace Tgstation.Server.Host.Controllers
public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
{
if (ApiHeaders == null)
return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
{
// Get the exact error
var errorMessage = "Missing API headers!";
try
{
var _ = new ApiHeaders(Request.GetTypedHeaders());
}
catch (InvalidOperationException ex)
{
errorMessage = ex.Message;
}
Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
return BadRequest(new Api.Models.ErrorMessage { Message = errorMessage });
}
if (ApiHeaders.IsTokenAuthentication)
return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
@@ -61,6 +61,18 @@ namespace Tgstation.Server.Host.Controllers
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
/// <summary>
/// Check if a given <paramref name="model"/> has a valid <see cref="Api.Models.Internal.User.Name"/> specified.
/// </summary>
/// <param name="model">The <see cref="UserUpdate"/> to check.</param>
/// <returns><see langword="null"/> if <paramref name="model"/> is valid, a <see cref="BadRequestObjectResult"/> otherwise.</returns>
BadRequestObjectResult CheckValidName(UserUpdate model)
{
if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
return BadRequest(new ErrorMessage { Message = "Username must not contain colons!" });
return null;
}
/// <inheritdoc />
[TgsAuthorize(AdministrationRights.WriteUsers)]
public override async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
@@ -78,6 +90,10 @@ namespace Tgstation.Server.Host.Controllers
if (!(model.Name == null ^ model.SystemIdentifier == null))
return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
var fail = CheckValidName(model);
if (fail != null)
return fail;
var dbUser = new Models.User
{
AdministrationRights = model.AdministrationRights ?? AdministrationRights.None,
@@ -154,6 +170,11 @@ namespace Tgstation.Server.Host.Controllers
originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
var fail = CheckValidName(model);
if (fail != null)
return fail;
originalUser.Name = model.Name ?? originalUser.Name;
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);