diff --git a/src/Raven.Server/RavenServer.cs b/src/Raven.Server/RavenServer.cs index 83a17c7a7a1..ebea33d2c0a 100644 --- a/src/Raven.Server/RavenServer.cs +++ b/src/Raven.Server/RavenServer.cs @@ -1490,6 +1490,7 @@ public class AuthenticateConnection : IHttpAuthenticationFeature public AuthenticateConnection() { + Console.WriteLine("New authenticated connection"); } public bool CanAccess(string database, bool requireAdmin, bool requireWrite) @@ -1561,6 +1562,8 @@ public void WaitingForTwoFactorAuthentication() public void SuccessfulTwoFactorAuthentication() { + Console.WriteLine("SuccessfulTwoFactorAuthentication::" + TwoFactorAuthRegistration); + //TODO: check if previous state was waiting for? _status = _statusAfterTwoFactorAuth; } @@ -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; @@ -1689,8 +1696,9 @@ internal AuthenticateConnection AuthenticateConnectionCertificate(X509Certificat _twoFactorAuthTimeByCertThumbprintExpiry.TryRemove(new KeyValuePair(certificate.Thumbprint, twoFactorAuthRegistration)); } } - if(hasTotpRecently == false) + if (hasTotpRecently == false) { + Console.WriteLine("AuthenticateConnectionCertificate::Waiting for two factor"); authenticationStatus.WaitingForTwoFactorAuthentication(); return authenticationStatus; } diff --git a/src/Raven.Server/Routing/RequestRouter.cs b/src/Raven.Server/Routing/RequestRouter.cs index e5ad947e27a..c4e97071619 100644 --- a/src/Raven.Server/Routing/RequestRouter.cs +++ b/src/Raven.Server/Routing/RequestRouter.cs @@ -150,9 +150,9 @@ 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) { @@ -160,6 +160,8 @@ public RouteInformation GetRoute(string method, string path, out RouteMatch matc 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); } } @@ -167,8 +169,14 @@ public RouteInformation GetRoute(string method, string path, out RouteMatch matc 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"; @@ -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: @@ -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 @@ -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; } diff --git a/src/Raven.Server/Web/Authentication/AdminCertificatesHandler.cs b/src/Raven.Server/Web/Authentication/AdminCertificatesHandler.cs index 33c1c95191e..69cee09476b 100644 --- a/src/Raven.Server/Web/Authentication/AdminCertificatesHandler.cs +++ b/src/Raven.Server/Web/Authentication/AdminCertificatesHandler.cs @@ -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() { @@ -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"); diff --git a/src/Raven.Server/Web/Authentication/TwoFactorAuthentication.cs b/src/Raven.Server/Web/Authentication/TwoFactorAuthentication.cs index 2c4237aeaba..e811a952dd1 100644 --- a/src/Raven.Server/Web/Authentication/TwoFactorAuthentication.cs +++ b/src/Raven.Server/Web/Authentication/TwoFactorAuthentication.cs @@ -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); diff --git a/src/Raven.Server/Web/Authentication/TwoFactorAuthenticationHandler.cs b/src/Raven.Server/Web/Authentication/TwoFactorAuthenticationHandler.cs index 61549063101..c534bf3f2d2 100644 --- a/src/Raven.Server/Web/Authentication/TwoFactorAuthenticationHandler.cs +++ b/src/Raven.Server/Web/Authentication/TwoFactorAuthenticationHandler.cs @@ -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; } @@ -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)) @@ -66,8 +65,7 @@ public async Task ValidateTotp() { period = TimeSpan.FromHours(2); } - var feature = (RavenServer.AuthenticateConnection)HttpContext.Features.Get(); - feature.SuccessfulTwoFactorAuthentication(); // enable access for the current connection + if (_auditLogger.IsInfoEnabled) { @@ -86,8 +84,8 @@ public async Task ValidateTotp() SameSite = SameSiteMode.Strict, Secure = true }); - - Server.RegisterTwoFactorAuthSuccess(new RavenServer.TwoFactorAuthRegistration + + RavenServer.TwoFactorAuthRegistration twoFactorAuthRegistration = new() { Thumbprint = clientCert.Thumbprint, Period = period, @@ -95,23 +93,31 @@ public async Task ValidateTotp() HasLimits = hasLimits, CsrfAccessToken = csrfAccessToken, ExpectedCookieValue = expectedCookieValue - }); + }; + + Server.RegisterTwoFactorAuthSuccess(twoFactorAuthRegistration); + + var feature = (RavenServer.AuthenticateConnection)HttpContext.Features.Get(); + 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) { @@ -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"); diff --git a/src/Raven.Server/Web/System/StudioHandler.cs b/src/Raven.Server/Web/System/StudioHandler.cs index 56bca68f70f..b664e43c42b 100644 --- a/src/Raven.Server/Web/System/StudioHandler.cs +++ b/src/Raven.Server/Web/System/StudioHandler.cs @@ -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() @@ -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"); } diff --git a/src/Raven.Studio/typescript/commands/auth/generateTwoFactorSecretCommand.ts b/src/Raven.Studio/typescript/commands/auth/generateTwoFactorSecretCommand.ts new file mode 100644 index 00000000000..1ff655bd2c0 --- /dev/null +++ b/src/Raven.Studio/typescript/commands/auth/generateTwoFactorSecretCommand.ts @@ -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; diff --git a/src/Raven.Studio/typescript/commands/auth/validateTwoFactorSecretCommand.ts b/src/Raven.Studio/typescript/commands/auth/validateTwoFactorSecretCommand.ts new file mode 100644 index 00000000000..5805caf4335 --- /dev/null +++ b/src/Raven.Studio/typescript/commands/auth/validateTwoFactorSecretCommand.ts @@ -0,0 +1,22 @@ +import commandBase = require("commands/commandBase"); +import endpoints = require("endpoints"); + +class validateTwoFactorSecretCommand extends commandBase { + + constructor(private secret: string) { + super(); + } + + execute(): JQueryPromise { + 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; diff --git a/src/Raven.Studio/typescript/commands/commandBase.ts b/src/Raven.Studio/typescript/commands/commandBase.ts index 7a9a65ce189..116490eee65 100644 --- a/src/Raven.Studio/typescript/commands/commandBase.ts +++ b/src/Raven.Studio/typescript/commands/commandBase.ts @@ -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 { @@ -117,7 +118,7 @@ class commandBase { }, false); }; - const defaultOptions = { + const defaultOptions: JQueryAjaxSettings = { url: url, data: args, dataType: "json", @@ -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: + } } }; diff --git a/src/Raven.Studio/typescript/main.ts b/src/Raven.Studio/typescript/main.ts index e426f2b2c0e..0c18415dda1 100644 --- a/src/Raven.Studio/typescript/main.ts +++ b/src/Raven.Studio/typescript/main.ts @@ -1,5 +1,7 @@ /// +import eulaShell from "viewmodels/eulaShell"; + require('../wwwroot/Content/css/fonts/icomoon.font'); import { overrideViews } from "./overrides/views"; @@ -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); diff --git a/src/Raven.Studio/typescript/models/auth/certificateModel.ts b/src/Raven.Studio/typescript/models/auth/certificateModel.ts index 6ad0a3341f3..ae47baed96f 100644 --- a/src/Raven.Studio/typescript/models/auth/certificateModel.ts +++ b/src/Raven.Studio/typescript/models/auth/certificateModel.ts @@ -35,6 +35,9 @@ class certificateModel { name = ko.observable(); securityClearance = ko.observable("ValidUser"); + requireTwoFactor = ko.observable(false); + authenticationKey = ko.observable(""); + certificateAsBase64 = ko.observable(); certificatePassphrase = ko.observable(); @@ -150,7 +153,9 @@ class certificateModel { Password: this.certificatePassphrase(), Permissions: this.serializePermissions(), SecurityClearance: this.securityClearance(), - NotAfter: this.expirationDateFormatted() + NotAfter: this.expirationDateFormatted(), + TwoFactorAuthenticationKey: this.requireTwoFactor() ? this.authenticationKey() : null, + //TODO: validatity perdio } } @@ -168,7 +173,8 @@ class certificateModel { Password: this.certificatePassphrase(), Permissions: this.serializePermissions(), SecurityClearance: this.securityClearance(), - NotAfter: this.expirationDateFormatted() + NotAfter: this.expirationDateFormatted(), + TwoFactorAuthenticationKey: this.requireTwoFactor() ? this.authenticationKey() : null, } } @@ -177,7 +183,8 @@ class certificateModel { Name: this.name(), Thumbprint: this.thumbprint(), SecurityClearance: this.securityClearance(), - Permissions: this.serializePermissions() + Permissions: this.serializePermissions(), + TwoFactorAuthenticationKey: this.requireTwoFactor() ? this.authenticationKey() : null, //TODO: do we want it? } } @@ -230,6 +237,8 @@ class certificateModel { model.thumbprint(dto.Thumbprint); model.thumbprints(dto.Thumbprints); + //TODO: what about updating ? + model.permissions(_.map(dto.Permissions, (access, databaseName) => { const permission = new certificatePermissionModel(); permission.accessLevel(access); diff --git a/src/Raven.Studio/typescript/viewmodels/manage/certificates.ts b/src/Raven.Studio/typescript/viewmodels/manage/certificates.ts index 2b4b87e4003..e700293311a 100644 --- a/src/Raven.Studio/typescript/viewmodels/manage/certificates.ts +++ b/src/Raven.Studio/typescript/viewmodels/manage/certificates.ts @@ -23,6 +23,8 @@ import getServerCertificateRenewalDateCommand = require("commands/auth/getServer import fileImporter = require("common/fileImporter"); import generalUtils = require("common/generalUtils"); import moment = require("moment"); +import generateTwoFactorSecretCommand from "commands/auth/generateTwoFactorSecretCommand"; +import { QRCode } from "qrcodejs"; type certificatesSortMode = "default" | "byNameAsc" | "byExpirationAsc" | "byValidFromAsc" | @@ -109,6 +111,9 @@ class certificates extends viewModelBase { sortModeText: KnockoutComputed; deleteExistingCertificate = ko.observable(false); + + // currently displayed QR Code + private qrCode: any; constructor() { super(); @@ -116,7 +121,7 @@ class certificates extends viewModelBase { this.bindToCurrentInstance("onCloseEdit", "save", "enterEditCertificateMode", "enterRegenerateCertificateMode", "deletePermission", "fileSelected", "copyThumbprint", "deleteCertificateConfirm", "renewServerCertificate", "canBeAutomaticallyRenewed", - "sortCertificates", "clearAllFilters", + "sortCertificates", "clearAllFilters", "syncQrCode", "addPermission","addPermissionWithBlink","addDatabase","addDatabaseWithBlink"); this.initObservables(); @@ -166,12 +171,68 @@ class certificates extends viewModelBase { }); this.model.subscribe(model => { + if (!model || !model.requireTwoFactor()) { + if (this.qrCode) { + this.qrCode.clear(); + this.qrCode = null; + } + } + if (model) { this.initPopover(); + + model.name.subscribe(() => this.syncQrCode()); + model.authenticationKey.subscribe(() => this.syncQrCode()); + model.requireTwoFactor.subscribe(twoFactor => { + if (twoFactor) { + this.generateSecret(model); + } + }); } }); } + syncQrCode(): void { + if (!this.model().requireTwoFactor() || !this.model().authenticationKey()) { + return; + } + + const secret = this.model().authenticationKey(); + const encodedIssuer = encodeURIComponent(location.hostname); + const encodedName = encodeURIComponent(this.model().name() ?? "Client Certificate"); + + const uri = `otpauth://totp/${encodedIssuer}:${encodedName}?secret=${secret}&issuer=${encodedIssuer}`; + + const qrContainer = document.getElementById("encryption_qrcode"); + + if (qrContainer.innerHTML && !this.qrCode) { + // clean up old instances + qrContainer.innerHTML = ""; + } + + if (!this.qrCode) { + this.qrCode = new QRCode(qrContainer, { + text: uri, + width: 256, + height: 256, + colorDark: "#000000", + colorLight: "#ffffff", + correctLevel: QRCode.CorrectLevel.Q + }); + } else { + this.qrCode.clear(); + this.qrCode.makeCode(uri); + } + } + + private generateSecret(model: certificateModel): void { + new generateTwoFactorSecretCommand() + .execute() + .done(key => { + model.authenticationKey(key.Secret); + }); + } + private filterCertificates(): void { this.certificates().forEach((certificate: unifiedCertificateDefinitionWithCache) => { certificate.Visible(this.isMatchingTextFilter(certificate) && @@ -704,6 +765,10 @@ class certificates extends viewModelBase { copyThumbprint(thumbprint: string): void { copyToClipboard.copy(thumbprint, "Thumbprint was copied to clipboard."); } + + copyAuthenticationKeyToClipboard(authenticationKey: string) { + copyToClipboard.copy(authenticationKey, "Authentication Key was copied to clipboard."); + } canEdit(model: unifiedCertificateDefinition) { return ko.pureComputed(() => { diff --git a/src/Raven.Studio/typescript/viewmodels/twoFactorShell.ts b/src/Raven.Studio/typescript/viewmodels/twoFactorShell.ts new file mode 100644 index 00000000000..e0ebe3897f6 --- /dev/null +++ b/src/Raven.Studio/typescript/viewmodels/twoFactorShell.ts @@ -0,0 +1,22 @@ +import viewModelBase from "viewmodels/viewModelBase"; +import validateTwoFactorSecretCommand from "commands/auth/validateTwoFactorSecretCommand"; + + +class twoFactorShell extends viewModelBase { + view = require("views/twoFactorShell.html"); + + + compositionComplete() { + super.compositionComplete(); + + const response = prompt("Enter 6 digits code"); + + //TODO: extra params! + + new validateTwoFactorSecretCommand(response) + .execute(); + } +} + + +export = twoFactorShell; diff --git a/src/Raven.Studio/wwwroot/App/views/manage/certificates.html b/src/Raven.Studio/wwwroot/App/views/manage/certificates.html index ec24d295672..08c64a4d7dd 100644 --- a/src/Raven.Studio/wwwroot/App/views/manage/certificates.html +++ b/src/Raven.Studio/wwwroot/App/views/manage/certificates.html @@ -483,6 +483,28 @@

Database Permissions

+
+
+ + +
+
+ + + +
+
+

QR Code:

+
+
+
+
+
+

diff --git a/src/Raven.Studio/wwwroot/App/views/twoFactorShell.html b/src/Raven.Studio/wwwroot/App/views/twoFactorShell.html new file mode 100644 index 00000000000..ee4941793a0 --- /dev/null +++ b/src/Raven.Studio/wwwroot/App/views/twoFactorShell.html @@ -0,0 +1,3 @@ +
+ 2FA +