Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ml054 committed Oct 26, 2023
1 parent 90b9d12 commit 7224d07
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 26 deletions.
12 changes: 10 additions & 2 deletions src/Raven.Server/RavenServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,7 @@ public class AuthenticateConnection : IHttpAuthenticationFeature

public AuthenticateConnection()
{
Console.WriteLine("New authenticated connection");
}

public bool CanAccess(string database, bool requireAdmin, bool requireWrite)
Expand Down Expand Up @@ -1561,6 +1562,8 @@ public void WaitingForTwoFactorAuthentication()

public void SuccessfulTwoFactorAuthentication()
{
Console.WriteLine("SuccessfulTwoFactorAuthentication::" + TwoFactorAuthRegistration);
//TODO: check if previous state was waiting for?
_status = _statusAfterTwoFactorAuth;
}

Expand Down Expand Up @@ -1669,17 +1672,21 @@ internal AuthenticateConnection AuthenticateConnectionCertificate(X509Certificat

if (cert.TryGet(nameof(PutCertificateCommand.TwoFactorAuthenticationKey), out string _))
{

Console.WriteLine("AuthenticateConnectionCertificate::New connection");
bool hasTotpRecently = false;
if (_twoFactorAuthTimeByCertThumbprintExpiry.TryGetValue(certificate.Thumbprint, out var twoFactorAuthRegistration))
{
if (Time.GetUtcNow() < twoFactorAuthRegistration.Expiry)
{
if (twoFactorAuthRegistration.HasLimits && twoFactorAuthRegistration.IpAddresses != null &&
if (twoFactorAuthRegistration.HasLimits && twoFactorAuthRegistration.IpAddresses != null && //TODO: what's the purpose?
Array.IndexOf(twoFactorAuthRegistration.IpAddresses, address.ToString()) == -1)
{
authenticationStatus.Status = AuthenticationStatus.TwoFactorAuthFromInvalidLimit;
return authenticationStatus;
}

Console.WriteLine("AuthenticateConnectionCertificate::Assigned existing two factor auth");

authenticationStatus.TwoFactorAuthRegistration = twoFactorAuthRegistration;
hasTotpRecently = true;
Expand All @@ -1689,8 +1696,9 @@ internal AuthenticateConnection AuthenticateConnectionCertificate(X509Certificat
_twoFactorAuthTimeByCertThumbprintExpiry.TryRemove(new KeyValuePair<string, TwoFactorAuthRegistration>(certificate.Thumbprint, twoFactorAuthRegistration));
}
}
if(hasTotpRecently == false)
if (hasTotpRecently == false)
{
Console.WriteLine("AuthenticateConnectionCertificate::Waiting for two factor");
authenticationStatus.WaitingForTwoFactorAuthentication();
return authenticationStatus;
}
Expand Down
18 changes: 15 additions & 3 deletions src/Raven.Server/Routing/RequestRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,25 +150,33 @@ public RouteInformation GetRoute(string method, string path, out RouteMatch matc
return (false, feature.Status);
}

if (feature.TwoFactorAuthRegistration is { HasLimits: true })
if (feature.TwoFactorAuthRegistration is { HasLimits: true }) //TODO: what's the purpose?
{
if (ValidateTwoFactorLimits(context, feature, out var msg) == false)
if (ValidateTwoFactorLimits(route, context, feature, out var msg) == false)
{
if (LoggingSource.AuditLog.IsInfoEnabled)
{
var auditLog = LoggingSource.AuditLog.GetLogger("RequestRouter", "Audit");
auditLog.Info($"Rejected request {context.Request.Method} {context.Request.GetFullUrl()} because: {msg}");
}

await UnlikelyFailAuthorizationAsync(context, database?.Name, feature, route.AuthorizationStatus);

return (false, RavenServer.AuthenticationStatus.TwoFactorAuthFromInvalidLimit);
}
}

return (true, feature.Status);
}

private bool ValidateTwoFactorLimits(HttpContext context, RavenServer.AuthenticateConnection feature, out string msg)
private bool ValidateTwoFactorLimits(RouteInformation routeInformation, HttpContext context, RavenServer.AuthenticateConnection feature, out string msg)
{
if (routeInformation.AuthorizationStatus == AuthorizationStatus.UnauthenticatedClients)
{
msg = null;
return true;
}

if (context.Request.Cookies.TryGetValue(TwoFactorAuthentication.CookieName, out var cookieStr) == false)
{
msg = $"Missing the '{TwoFactorAuthentication.CookieName}' in the request";
Expand Down Expand Up @@ -233,6 +241,7 @@ internal bool CanAccessRoute(RouteInformation route, HttpContext context, string
switch (feature.Status)
{
case RavenServer.AuthenticationStatus.TwoFactorAuthFromInvalidLimit:
case RavenServer.AuthenticationStatus.TwoFactorAuthNotProvided:
case RavenServer.AuthenticationStatus.NoCertificateProvided:
case RavenServer.AuthenticationStatus.Expired:
case RavenServer.AuthenticationStatus.NotYetValid:
Expand Down Expand Up @@ -517,6 +526,8 @@ public static async ValueTask UnlikelyFailAuthorizationAsync(HttpContext context
}
else if (feature.Status == RavenServer.AuthenticationStatus.TwoFactorAuthFromInvalidLimit)
{
statusCode = (int)HttpStatusCode.PreconditionRequired;
//TODO: rework message below
message = $"The supplied client certificate '{name}' requires two factor authorization and is limited to a specified IP address, but this request came from a different IP address. Please POST the relevant TOTP value to /authentication/2fa to register this IP address";
}
else
Expand Down Expand Up @@ -550,6 +561,7 @@ public static async ValueTask UnlikelyFailAuthorizationAsync(HttpContext context
{
context.Response.StatusCode = (int)HttpStatusCode.Redirect;
context.Response.Headers["Location"] = "/auth-error.html?err=" + Uri.EscapeDataString(message);

return;
}

Expand Down
20 changes: 20 additions & 0 deletions src/Raven.Server/Web/Authentication/AdminCertificatesHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ namespace Raven.Server.Web.Authentication
{
public class AdminCertificatesHandler : ServerRequestHandler
{
[RavenAction("/admin/certificates/2fa/generate", "GET", AuthorizationStatus.Operator)]
public async Task GenerateSecret()
{
await ServerStore.EnsureNotPassiveAsync();

var secret = TwoFactorAuthentication.GenerateSecret();

using (ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext context))
using (context.OpenReadTransaction())
await using (var writer = new AsyncBlittableJsonTextWriter(context, ResponseBodyStream()))
{
writer.WriteStartObject();
writer.WritePropertyName("Secret");
writer.WriteString(secret);
writer.WriteEndObject();
}
}

[RavenAction("/admin/certificates", "POST", AuthorizationStatus.Operator, DisableOnCpuCreditsExhaustion = true)]
public async Task Generate()
{
Expand All @@ -51,6 +69,8 @@ public async Task Generate()
operationId = ServerStore.Operations.GetNextOperationId();

var stream = TryGetRequestFromStream("Options") ?? RequestBodyStream();

//TODO: add 2fa!

var certificateJson = await ctx.ReadForDiskAsync(stream, "certificate-generation");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static string GenerateQrCodeUri(string secret, string host, string name)
// https://github.com/dotnet/aspnetcore/blob/6a7bcda42de7b98196b38924cc354216eba57c9b/src/Identity/Extensions.Core/src/Rfc6238AuthenticationService.cs#L15
public static class Rfc6238AuthenticationService
{
private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(3);
private static readonly TimeSpan _timestep = TimeSpan.FromSeconds(30);
private static readonly Encoding _encoding = new UTF8Encoding(false, true);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public async Task ValidateTotp()
using var _ = ServerStore.ContextPool.AllocateOperationContext(out TransactionOperationContext ctx);
ctx.OpenReadTransaction();

bool hasLimits = GetBoolValueQueryString("hasLimits") ?? false;
var ipsStrVals = GetStringValuesQueryString("ip");
bool hasLimits = GetBoolValueQueryString("hasLimits", false) ?? true; //tODO: default to false?
var ipsStrVals = GetStringValuesQueryString("ip", false);
var ips = ipsStrVals.Count == 0 ? new[] { HttpContext.Connection.RemoteIpAddress?.ToString() } : ipsStrVals.ToArray();

var clientCert = GetCurrentCertificate();

if (clientCert == null)
{
ReplyWith(ctx, "Two factor authentication requires that you'll use a client certificate, but none was provided.", HttpStatusCode.BadRequest);
await ReplyWith(ctx, "Two factor authentication requires that you'll use a client certificate, but none was provided.", HttpStatusCode.BadRequest);
return;
}

Expand All @@ -47,17 +47,16 @@ public async Task ValidateTotp()
var certificate = ServerStore.Cluster.GetCertificateByThumbprint(ctx, clientCert.Thumbprint);
if (certificate == null)
{
ReplyWith(ctx, $"The certificate {clientCert.Thumbprint} ({clientCert.FriendlyName}) is not known to the server", HttpStatusCode.BadRequest);
await ReplyWith(ctx, $"The certificate {clientCert.Thumbprint} ({clientCert.FriendlyName}) is not known to the server", HttpStatusCode.BadRequest);
return;
}

if (certificate.TryGet(nameof(PutCertificateCommand.TwoFactorAuthenticationKey), out string key) == false)
{
ReplyWith(ctx, $"The certificate {clientCert.Thumbprint} ({clientCert.FriendlyName}) is not set up for two factor authentication", HttpStatusCode.BadRequest);
await ReplyWith(ctx, $"The certificate {clientCert.Thumbprint} ({clientCert.FriendlyName}) is not set up for two factor authentication", HttpStatusCode.BadRequest);
return;
}


input.TryGet("Token", out int token);

if (TwoFactorAuthentication.ValidateCode(key, token))
Expand All @@ -66,8 +65,7 @@ public async Task ValidateTotp()
{
period = TimeSpan.FromHours(2);
}
var feature = (RavenServer.AuthenticateConnection)HttpContext.Features.Get<IHttpAuthenticationFeature>();
feature.SuccessfulTwoFactorAuthentication(); // enable access for the current connection


if (_auditLogger.IsInfoEnabled)
{
Expand All @@ -86,32 +84,40 @@ public async Task ValidateTotp()
SameSite = SameSiteMode.Strict,
Secure = true
});
Server.RegisterTwoFactorAuthSuccess(new RavenServer.TwoFactorAuthRegistration

RavenServer.TwoFactorAuthRegistration twoFactorAuthRegistration = new()
{
Thumbprint = clientCert.Thumbprint,
Period = period,
IpAddresses = ips,
HasLimits = hasLimits,
CsrfAccessToken = csrfAccessToken,
ExpectedCookieValue = expectedCookieValue
});
};

Server.RegisterTwoFactorAuthSuccess(twoFactorAuthRegistration);

var feature = (RavenServer.AuthenticateConnection)HttpContext.Features.Get<IHttpAuthenticationFeature>();
feature.TwoFactorAuthRegistration = twoFactorAuthRegistration;
feature.SuccessfulTwoFactorAuthentication(); // enable access for the current connection

HttpContext.Response.StatusCode = (int)HttpStatusCode.Accepted;
using (var writer = new BlittableJsonTextWriter(ctx, ResponseBodyStream()))
await using (var writer = new AsyncBlittableJsonTextWriter(ctx, ResponseBodyStream()))
{
writer.WriteStartObject();
writer.WritePropertyName("Token");
writer.WriteString(csrfAccessToken);
//TODO: expose expiration?
writer.WriteEndObject();
}
}
else
{
ReplyWith(ctx, $"Wrong token provided for {clientCert.Thumbprint} ({clientCert.FriendlyName})", HttpStatusCode.NotAcceptable);
await ReplyWith(ctx, $"Wrong token provided for {clientCert.Thumbprint} ({clientCert.FriendlyName})", HttpStatusCode.NotAcceptable);
}
}

private void ReplyWith(TransactionOperationContext ctx, string err, HttpStatusCode httpStatusCode)
private async Task ReplyWith(TransactionOperationContext ctx, string err, HttpStatusCode httpStatusCode)
{
if (_auditLogger.IsInfoEnabled)
{
Expand All @@ -120,7 +126,7 @@ private void ReplyWith(TransactionOperationContext ctx, string err, HttpStatusCo
$"Two factor auth failure from IP: {HttpContext.Connection.RemoteIpAddress} with cert: '{clientCert?.Thumbprint ?? "None"}/{clientCert?.Subject ?? "None"}' because: {err}");
}
HttpContext.Response.StatusCode = (int)httpStatusCode;
using (var writer = new BlittableJsonTextWriter(ctx, ResponseBodyStream()))
await using (var writer = new AsyncBlittableJsonTextWriter(ctx, ResponseBodyStream()))
{
writer.WriteStartObject();
writer.WritePropertyName("Error");
Expand Down
11 changes: 11 additions & 0 deletions src/Raven.Server/Web/System/StudioHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ public Task GetEulaFile()
);
return GetStudioFileInternal(serverRelativeFileName);
}

[RavenAction("/2fa/index.html", "GET", AuthorizationStatus.UnauthenticatedClients)]
public Task GetTwoFactorIndexFile()
{
//TODO: if 2fa provided redirect to studio!

return GetStudioFileInternal("index.html");
}

[RavenAction("/wizard/index.html", "GET", AuthorizationStatus.UnauthenticatedClients)]
public Task GetSetupIndexFile()
Expand Down Expand Up @@ -270,6 +278,9 @@ public Task GetStudioIndexFile()
HttpContext.Response.StatusCode = (int)HttpStatusCode.TemporaryRedirect;
return Task.CompletedTask;
}

//TODO: if request contains cookie for authenticated 2fa, then include CSRF is index.html meta tag
//TODO: it should allow us to use session with-in single browser - each new connection / studio will get same CSRF token based on cookie

return GetStudioFileInternal("index.html");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import commandBase = require("commands/commandBase");
import endpoints = require("endpoints");

class generateTwoFactorSecretCommand extends commandBase {

execute(): JQueryPromise<{ Secret: string }> {
const url = endpoints.global.adminCertificates.adminCertificates2faGenerate;

return this.query<{ Secret: string }>(url, null, null)
.fail((response: JQueryXHR) => this.reportError("Unable to generate authentication key", response.responseText, response.statusText));
}
}

export = generateTwoFactorSecretCommand;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import commandBase = require("commands/commandBase");
import endpoints = require("endpoints");

class validateTwoFactorSecretCommand extends commandBase {

constructor(private secret: string) {
super();
}

execute(): JQueryPromise<void> {
const url = endpoints.global.twoFactorAuthentication.authentication2fa;

const payload = {
Token: this.secret
}

return this.post<{ Secret: string }>(url, JSON.stringify(payload), null)
.fail((response: JQueryXHR) => this.reportError("Unable to authenticate with 2FA", response.responseText, response.statusText));
}
}

export = validateTwoFactorSecretCommand;
8 changes: 7 additions & 1 deletion src/Raven.Studio/typescript/commands/commandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import messagePublisher = require("common/messagePublisher");
import database = require("models/resources/database");
import appUrl = require("common/appUrl");
import protractedCommandsDetector = require("common/notifications/protractedCommandsDetector");
import router from "plugins/router";

/// Commands encapsulate a read or write operation to the database and support progress notifications and common AJAX related functionality.
class commandBase {
Expand Down Expand Up @@ -117,7 +118,7 @@ class commandBase {
}, false);
};

const defaultOptions = {
const defaultOptions: JQueryAjaxSettings = {
url: url,
data: args,
dataType: "json",
Expand All @@ -128,6 +129,11 @@ class commandBase {
const xhr = new XMLHttpRequest();
xhrConfiguration(xhr);
return xhr;
},
statusCode: {
428: function () {
window.location.href = "https://a.marcin2010.development.run:4433/2fa/index.html" //TODO:
}
}
};

Expand Down
5 changes: 5 additions & 0 deletions src/Raven.Studio/typescript/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// <reference path="../typings/tsd.d.ts" />

import eulaShell from "viewmodels/eulaShell";

require('../wwwroot/Content/css/fonts/icomoon.font');

import { overrideViews } from "./overrides/views";
Expand Down Expand Up @@ -57,6 +59,9 @@ app.start().then(() => {
} else if (window.location.pathname.startsWith("/eula")) {
const eulaShell = require("viewmodels/eulaShell");
app.setRoot(eulaShell);
} else if (window.location.pathname.startsWith("/2fa")) {
const twoFactorShell = require("viewmodels/twoFactorShell");
app.setRoot(twoFactorShell);
} else {
const setupShell = require("viewmodels/wizard/setupShell");
app.setRoot(setupShell);
Expand Down
Loading

0 comments on commit 7224d07

Please sign in to comment.