mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 08:33:19 +01:00
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user