mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
fixes for configuring u2f device
This commit is contained in:
@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Bit.Core;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Bit.Api.Controllers
|
namespace Bit.Api.Controllers
|
||||||
{
|
{
|
||||||
@ -17,13 +19,16 @@ namespace Bit.Api.Controllers
|
|||||||
public class TwoFactorController : Controller
|
public class TwoFactorController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly UserManager<User> _userManager;
|
private readonly UserManager<User> _userManager;
|
||||||
|
|
||||||
public TwoFactorController(
|
public TwoFactorController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
|
GlobalSettings globalSettings,
|
||||||
UserManager<User> userManager)
|
UserManager<User> userManager)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_globalSettings = globalSettings;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +122,7 @@ namespace Bit.Api.Controllers
|
|||||||
{
|
{
|
||||||
var user = await CheckPasswordAsync(model.MasterPasswordHash);
|
var user = await CheckPasswordAsync(model.MasterPasswordHash);
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f);
|
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.U2f);
|
||||||
if(!provider.Enabled || (provider?.MetaData != null && provider.MetaData.Count > 0))
|
if(provider == null || !provider.Enabled || (provider.MetaData?.Count ?? 0) > 0)
|
||||||
{
|
{
|
||||||
var reg = await _userService.StartU2fRegistrationAsync(user);
|
var reg = await _userService.StartU2fRegistrationAsync(user);
|
||||||
var response = new TwoFactorU2fResponseModel(user, provider, reg);
|
var response = new TwoFactorU2fResponseModel(user, provider, reg);
|
||||||
@ -130,6 +135,34 @@ namespace Bit.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("~/app-id.json")]
|
||||||
|
//[Produces("application/fido.trusted-apps+json")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public string GetU2fAppId()
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(new
|
||||||
|
{
|
||||||
|
trustedFacets = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
version = new
|
||||||
|
{
|
||||||
|
major = 1,
|
||||||
|
minor = 1
|
||||||
|
},
|
||||||
|
ids = new string[]
|
||||||
|
{
|
||||||
|
_globalSettings.U2f.AppId,
|
||||||
|
//"ios:bundle-id:com.8bit.bitwarden",
|
||||||
|
//"android:apk-key-hash:585215fd5153209a7e246f53286035838a0be227",
|
||||||
|
//"chrome-extension://nngceckbapebfimnlniiiahkandclblb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPut("u2f")]
|
[HttpPut("u2f")]
|
||||||
[HttpPost("u2f")]
|
[HttpPost("u2f")]
|
||||||
public async Task<TwoFactorU2fResponseModel> PutU2f([FromBody]TwoFactorU2fRequestModel model)
|
public async Task<TwoFactorU2fResponseModel> PutU2f([FromBody]TwoFactorU2fRequestModel model)
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:4000",
|
"applicationUrl": "http://localhost:4000",
|
||||||
"sslPort": 0
|
"sslPort": 44377
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
|
@ -153,9 +153,9 @@ namespace Bit.Api
|
|||||||
// Add IdentityServer to the request pipeline.
|
// Add IdentityServer to the request pipeline.
|
||||||
app.UseIdentityServer();
|
app.UseIdentityServer();
|
||||||
app.UseIdentityServerAuthentication(
|
app.UseIdentityServerAuthentication(
|
||||||
GetIdentityOptions(env, IdentityServerAuthority(env, "identity", "33656"), "3"));
|
GetIdentityOptions(env, IdentityServerAuthority(env, "identity", "44392"), "3"));
|
||||||
app.UseIdentityServerAuthentication(
|
app.UseIdentityServerAuthentication(
|
||||||
GetIdentityOptions(env, IdentityServerAuthority(env, "api", "4000"), "2"));
|
GetIdentityOptions(env, IdentityServerAuthority(env, "api", "44377"), "2"));
|
||||||
|
|
||||||
// Add current context
|
// Add current context
|
||||||
app.UseMiddleware<CurrentContextMiddleware>();
|
app.UseMiddleware<CurrentContextMiddleware>();
|
||||||
@ -195,7 +195,7 @@ namespace Bit.Api
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return $"http://localhost:{port}";
|
return $"https://localhost:{port}";
|
||||||
//return $"http://192.168.1.6:{port}"; // Desktop external
|
//return $"http://192.168.1.6:{port}"; // Desktop external
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"baseVaultUri": "https://preview-vault.bitwarden.com/#"
|
"baseVaultUri": "https://preview-vault.bitwarden.com/#",
|
||||||
|
"baseApiUri": "https://preview-api.bitwarden.com/",
|
||||||
|
"u2f": {
|
||||||
|
"appId": "https://preview-vault.bitwarden.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"baseVaultUri": "https://vault.bitwarden.com/#"
|
"baseVaultUri": "https://vault.bitwarden.com/#",
|
||||||
|
"baseApiUri": "https://api.bitwarden.com/",
|
||||||
|
"u2f": {
|
||||||
|
"appId": "https://vault.bitwarden.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"baseVaultUri": "https://vault.bitwarden.com/#"
|
"baseVaultUri": "https://vault.bitwarden.com/#",
|
||||||
|
"baseApiUri": "https://api.bitwarden.com/",
|
||||||
|
"u2f": {
|
||||||
|
"appId": "https://vault.bitwarden.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"siteName": "bitwarden",
|
"siteName": "bitwarden",
|
||||||
"baseVaultUri": "http://localhost:4001/#",
|
"baseVaultUri": "http://localhost:4001/#",
|
||||||
|
"baseApiUri": "http://localhost:4000/",
|
||||||
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
||||||
"stripeApiKey": "SECRET",
|
"stripeApiKey": "SECRET",
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
@ -47,7 +48,7 @@
|
|||||||
"aKey": "SECRET"
|
"aKey": "SECRET"
|
||||||
},
|
},
|
||||||
"u2f": {
|
"u2f": {
|
||||||
"appId": "https://bitwarden.com"
|
"appId": "https://localhost:4001"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"IpRateLimitOptions": {
|
"IpRateLimitOptions": {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"siteName": "bitwarden",
|
"siteName": "bitwarden",
|
||||||
"baseVaultUri": "http://localhost:4001/#",
|
"baseVaultUri": "http://localhost:4001/#",
|
||||||
|
"baseApiUri": "http://localhost:4000/",
|
||||||
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
||||||
"stripeApiKey": "SECRET",
|
"stripeApiKey": "SECRET",
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
|
@ -49,13 +49,14 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="1.1.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Portable.BouncyCastle" Version="1.8.1.2" />
|
||||||
<PackageReference Include="RazorLight" Version="1.1.0" />
|
<PackageReference Include="RazorLight" Version="1.1.0" />
|
||||||
<PackageReference Include="Sendgrid" Version="9.2.0" />
|
<PackageReference Include="Sendgrid" Version="9.2.0" />
|
||||||
<PackageReference Include="PushSharp" Version="4.0.10" />
|
<PackageReference Include="PushSharp" Version="4.0.10" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="1.4.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="1.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.AzureDocumentDB" Version="3.6.1" />
|
<PackageReference Include="Serilog.Sinks.AzureDocumentDB" Version="3.6.1" />
|
||||||
<PackageReference Include="Stripe.net" Version="7.8.0" />
|
<PackageReference Include="Stripe.net" Version="7.8.0" />
|
||||||
<PackageReference Include="U2F.Core" Version="1.0.1" />
|
<PackageReference Include="u2flib" Version="1.0.5" />
|
||||||
<PackageReference Include="WindowsAzure.Storage" Version="8.1.1" />
|
<PackageReference Include="WindowsAzure.Storage" Version="8.1.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.0.1" />
|
<PackageReference Include="Otp.NET" Version="1.0.1" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.0.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.0.0" />
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
{
|
{
|
||||||
public virtual string SiteName { get; set; }
|
public virtual string SiteName { get; set; }
|
||||||
public virtual string BaseVaultUri { get; set; }
|
public virtual string BaseVaultUri { get; set; }
|
||||||
|
public virtual string BaseApiUri { get; set; }
|
||||||
public virtual string JwtSigningKey { get; set; }
|
public virtual string JwtSigningKey { get; set; }
|
||||||
public virtual string StripeApiKey { get; set; }
|
public virtual string StripeApiKey { get; set; }
|
||||||
public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings();
|
public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings();
|
||||||
|
@ -5,12 +5,13 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using U2F.Core.Models;
|
|
||||||
using U2fLib = U2F.Core.Crypto.U2F;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using U2F.Core.Exceptions;
|
using u2flib.Data;
|
||||||
|
using u2flib;
|
||||||
|
using u2flib.Data.Messages;
|
||||||
|
using u2flib.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.Identity
|
namespace Bit.Core.Identity
|
||||||
{
|
{
|
||||||
@ -65,7 +66,7 @@ namespace Bit.Core.Identity
|
|||||||
{
|
{
|
||||||
var registration = new DeviceRegistration(key.KeyHandleBytes, key.PublicKeyBytes,
|
var registration = new DeviceRegistration(key.KeyHandleBytes, key.PublicKeyBytes,
|
||||||
key.CertificateBytes, key.Counter);
|
key.CertificateBytes, key.Counter);
|
||||||
var auth = U2fLib.StartAuthentication(_globalSettings.U2f.AppId, registration);
|
var auth = U2F.StartAuthentication(Utilities.CoreHelpers.U2fAppIdUrl(_globalSettings), registration);
|
||||||
|
|
||||||
// Maybe move this to a bulk create when we support more than 1 key?
|
// Maybe move this to a bulk create when we support more than 1 key?
|
||||||
await _u2fRepository.CreateAsync(new U2f
|
await _u2fRepository.CreateAsync(new U2f
|
||||||
@ -116,7 +117,7 @@ namespace Bit.Core.Identity
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var authenticateResponse = BaseModel.FromJson<AuthenticateResponse>(token);
|
var authenticateResponse = DataObject.FromJson<AuthenticateResponse>(token);
|
||||||
var key = keys.FirstOrDefault(f => f.KeyHandle == authenticateResponse.KeyHandle);
|
var key = keys.FirstOrDefault(f => f.KeyHandle == authenticateResponse.KeyHandle);
|
||||||
|
|
||||||
if(key == null)
|
if(key == null)
|
||||||
@ -139,7 +140,7 @@ namespace Bit.Core.Identity
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var auth = new StartedAuthentication(challenge.Challenge, challenge.AppId, challenge.KeyHandle);
|
var auth = new StartedAuthentication(challenge.Challenge, challenge.AppId, challenge.KeyHandle);
|
||||||
U2fLib.FinishAuthentication(auth, authenticateResponse, registration);
|
U2F.FinishAuthentication(auth, authenticateResponse, registration);
|
||||||
}
|
}
|
||||||
catch(U2fException)
|
catch(U2fException)
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@ namespace Bit.Core.Models.Api
|
|||||||
{
|
{
|
||||||
Challenge = new ChallengeModel(user, registration);
|
Challenge = new ChallengeModel(user, registration);
|
||||||
}
|
}
|
||||||
Enabled = provider.Enabled;
|
Enabled = provider?.Enabled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TwoFactorU2fResponseModel(User user)
|
public TwoFactorU2fResponseModel(User user)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using U2F.Core.Utils;
|
using u2flib.Util;
|
||||||
|
|
||||||
namespace Bit.Core.Models
|
namespace Bit.Core.Models
|
||||||
{
|
{
|
||||||
|
@ -10,11 +10,12 @@ using System.Linq;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using U2fLib = U2F.Core.Crypto.U2F;
|
|
||||||
using U2F.Core.Models;
|
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using U2F.Core.Utils;
|
using u2flib.Data.Messages;
|
||||||
|
using u2flib.Util;
|
||||||
|
using u2flib;
|
||||||
|
using u2flib.Data;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@ -219,7 +220,7 @@ namespace Bit.Core.Services
|
|||||||
public async Task<U2fRegistration> StartU2fRegistrationAsync(User user)
|
public async Task<U2fRegistration> StartU2fRegistrationAsync(User user)
|
||||||
{
|
{
|
||||||
await _u2fRepository.DeleteManyByUserIdAsync(user.Id);
|
await _u2fRepository.DeleteManyByUserIdAsync(user.Id);
|
||||||
var reg = U2fLib.StartRegistration(_globalSettings.U2f.AppId);
|
var reg = U2F.StartRegistration(Utilities.CoreHelpers.U2fAppIdUrl(_globalSettings));
|
||||||
await _u2fRepository.CreateAsync(new U2f
|
await _u2fRepository.CreateAsync(new U2f
|
||||||
{
|
{
|
||||||
AppId = reg.AppId,
|
AppId = reg.AppId,
|
||||||
@ -249,11 +250,11 @@ namespace Bit.Core.Services
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var registerResponse = BaseModel.FromJson<RegisterResponse>(deviceResponse);
|
var registerResponse = DataObject.FromJson<RegisterResponse>(deviceResponse);
|
||||||
|
|
||||||
var challenge = challenges.OrderBy(i => i.Id).Last(i => i.KeyHandle == null);
|
var challenge = challenges.OrderBy(i => i.Id).Last(i => i.KeyHandle == null);
|
||||||
var statedReg = new StartedRegistration(challenge.Challenge, challenge.AppId);
|
var statedReg = new StartedRegistration(challenge.Challenge, challenge.AppId);
|
||||||
var reg = U2fLib.FinishRegistration(statedReg, registerResponse);
|
var reg = U2F.FinishRegistration(statedReg, registerResponse);
|
||||||
|
|
||||||
await _u2fRepository.DeleteManyByUserIdAsync(user.Id);
|
await _u2fRepository.DeleteManyByUserIdAsync(user.Id);
|
||||||
|
|
||||||
|
@ -119,5 +119,11 @@ namespace Bit.Core.Utilities
|
|||||||
{
|
{
|
||||||
return _epoc.AddMilliseconds(milliseconds);
|
return _epoc.AddMilliseconds(milliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string U2fAppIdUrl(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
//return $"{globalSettings.BaseApiUri}app-id.json";
|
||||||
|
return globalSettings.U2f.AppId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"iisSettings": {
|
"iisSettings": {
|
||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:33656/",
|
"applicationUrl": "http://localhost:33656/",
|
||||||
"sslPort": 0
|
"sslPort": 44392
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"globalSettings": {
|
"globalSettings": {
|
||||||
"siteName": "bitwarden",
|
"siteName": "bitwarden",
|
||||||
"baseVaultUri": "http://localhost:4001/#",
|
"baseVaultUri": "http://localhost:4001/#",
|
||||||
|
"baseApiUri": "http://localhost:4000/",
|
||||||
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
"jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)",
|
||||||
"stripeApiKey": "SECRET",
|
"stripeApiKey": "SECRET",
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
|
@ -12,7 +12,6 @@ BEGIN
|
|||||||
|
|
||||||
INSERT INTO [dbo].[U2f]
|
INSERT INTO [dbo].[U2f]
|
||||||
(
|
(
|
||||||
[Id],
|
|
||||||
[UserId],
|
[UserId],
|
||||||
[KeyHandle],
|
[KeyHandle],
|
||||||
[Challenge],
|
[Challenge],
|
||||||
@ -22,7 +21,6 @@ BEGIN
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@Id,
|
|
||||||
@UserId,
|
@UserId,
|
||||||
@KeyHandle,
|
@KeyHandle,
|
||||||
@Challenge,
|
@Challenge,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
CREATE TABLE [dbo].[U2f] (
|
CREATE TABLE [dbo].[U2f] (
|
||||||
[Id] INT IDENTITY (1, 1) NOT NULL,
|
[Id] INT IDENTITY (1, 1) NOT NULL,
|
||||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[KeyHandle] VARCHAR (50) NOT NULL,
|
[KeyHandle] VARCHAR (MAX) NULL,
|
||||||
[Challenge] VARCHAR (50) NOT NULL,
|
[Challenge] VARCHAR (MAX) NOT NULL,
|
||||||
[AppId] VARCHAR (50) NOT NULL,
|
[AppId] VARCHAR (50) NOT NULL,
|
||||||
[Version] VARCHAR (50) NOT NULL,
|
[Version] VARCHAR (20) NOT NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
CONSTRAINT [PK_U2f] PRIMARY KEY CLUSTERED ([Id] ASC),
|
CONSTRAINT [PK_U2f] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
CONSTRAINT [FK_U2f_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
CONSTRAINT [FK_U2f_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
||||||
|
Reference in New Issue
Block a user