Remove OpenIddict (#1244)

This commit is contained in:
Andrew Camilleri 2020-02-24 16:40:04 +01:00 committed by GitHub
parent d16a4334cb
commit 276a9a95f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 182 additions and 1690 deletions

View file

@ -7,7 +7,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data namespace BTCPayServer.Data
{ {
@ -261,28 +260,6 @@ namespace BTCPayServer.Data
.HasOne(o => o.WalletData) .HasOne(o => o.WalletData)
.WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade);
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
if (Database.IsSqlite() && !_designTime)
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
// use the DateTimeOffsetToBinaryConverter
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
// This only supports millisecond precision, but should be sufficient for most use cases.
foreach (var entityType in builder.Model.GetEntityTypes())
{
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
foreach (var property in properties)
{
builder
.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter());
}
}
}
} }
} }

View file

@ -21,8 +21,6 @@ namespace BTCPayServer.Data
get; set; get; set;
} }
public List<BTCPayOpenIdClient> OpenIdClients { get; set; }
public List<StoredFile> StoredFiles public List<StoredFile> StoredFiles
{ {
get; get;

View file

@ -1,6 +0,0 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data
{
public class BTCPayOpenIdAuthorization : OpenIddictAuthorization<string, BTCPayOpenIdClient, BTCPayOpenIdToken> { }
}

View file

@ -1,10 +0,0 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data
{
public class BTCPayOpenIdClient: OpenIddictApplication<string, BTCPayOpenIdAuthorization, BTCPayOpenIdToken>
{
public string ApplicationUserId { get; set; }
public ApplicationUser ApplicationUser { get; set; }
}
}

View file

@ -1,6 +0,0 @@
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data
{
public class BTCPayOpenIdToken : OpenIddictToken<string, BTCPayOpenIdClient, BTCPayOpenIdAuthorization> { }
}

View file

@ -0,0 +1,169 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class Remove_OpenIddict : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OpenIddictScopes");
migrationBuilder.DropTable(
name: "OpenIddictTokens");
migrationBuilder.DropTable(
name: "OpenIddictAuthorizations");
migrationBuilder.DropTable(
name: "OpenIddictApplications");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
int? maxLength = this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)255 : null;
migrationBuilder.CreateTable(
name: "OpenIddictApplications",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
ClientId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
ClientSecret = table.Column<string>(type: "TEXT", nullable: true),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
ConsentType = table.Column<string>(type: "TEXT", nullable: true),
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
Permissions = table.Column<string>(type: "TEXT", nullable: true),
PostLogoutRedirectUris = table.Column<string>(type: "TEXT", nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
RedirectUris = table.Column<string>(type: "TEXT", nullable: true),
Requirements = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "OpenIddictScopes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
Properties = table.Column<string>(type: "TEXT", nullable: true),
Resources = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictScopes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OpenIddictAuthorizations",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
ApplicationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
Scopes = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false),
Subject = table.Column<string>(type: "TEXT", maxLength: 450, nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "OpenIddictTokens",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false, maxLength: maxLength),
ApplicationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
AuthorizationId = table.Column<string>(type: "TEXT", nullable: true, maxLength: maxLength),
ConcurrencyToken = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
CreationDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
ExpirationDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Payload = table.Column<string>(type: "TEXT", nullable: true),
Properties = table.Column<string>(type: "TEXT", nullable: true),
ReferenceId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Status = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false),
Subject = table.Column<string>(type: "TEXT", maxLength: 450, nullable: true),
Type = table.Column<string>(type: "TEXT", maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "OpenIddictAuthorizations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_OpenIddictApplications_ApplicationUserId",
table: "OpenIddictApplications",
column: "ApplicationUserId");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictApplications_ClientId",
table: "OpenIddictApplications",
column: "ClientId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type",
table: "OpenIddictAuthorizations",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
migrationBuilder.CreateIndex(
name: "IX_OpenIddictScopes_Name",
table: "OpenIddictScopes",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_AuthorizationId",
table: "OpenIddictTokens",
column: "AuthorizationId");
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ReferenceId",
table: "OpenIddictTokens",
column: "ReferenceId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type",
table: "OpenIddictTokens",
columns: new[] { "ApplicationId", "Status", "Subject", "Type" });
}
}
}

View file

@ -160,164 +160,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers"); b.ToTable("AspNetUsers");
}); });
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Scopes")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<string>("Subject")
.HasColumnType("TEXT")
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictAuthorizations");
});
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(100);
b.Property<string>("ClientSecret")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("ConsentType")
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("RedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Requirements")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("ApplicationUserId");
b.HasIndex("ClientId")
.IsUnique();
b.ToTable("OpenIddictApplications");
});
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("AuthorizationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<DateTimeOffset?>("CreationDate")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("ExpirationDate")
.HasColumnType("TEXT");
b.Property<string>("Payload")
.HasColumnType("TEXT");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("ReferenceId")
.HasColumnType("TEXT")
.HasMaxLength(100);
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<string>("Subject")
.HasColumnType("TEXT")
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
b.HasIndex("AuthorizationId");
b.HasIndex("ReferenceId")
.IsUnique();
b.HasIndex("ApplicationId", "Status", "Subject", "Type");
b.ToTable("OpenIddictTokens");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{ {
b.Property<string>("InvoiceDataId") b.Property<string>("InvoiceDataId")
@ -812,42 +654,6 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUserTokens"); b.ToTable("AspNetUserTokens");
}); });
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(200);
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Resources")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("OpenIddictScopes");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b => modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{ {
b.HasOne("BTCPayServer.Data.StoreData", "StoreData") b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -877,31 +683,6 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
{
b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application")
.WithMany("Authorizations")
.HasForeignKey("ApplicationId");
});
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("OpenIdClients")
.HasForeignKey("ApplicationUserId");
});
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
{
b.HasOne("BTCPayServer.Data.BTCPayOpenIdClient", "Application")
.WithMany("Tokens")
.HasForeignKey("ApplicationId");
b.HasOne("BTCPayServer.Data.BTCPayOpenIdAuthorization", "Authorization")
.WithMany("Tokens")
.HasForeignKey("AuthorizationId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b => modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{ {
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData") b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

View file

@ -1,428 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Security.Claims;
using BTCPayServer.Tests.Logging;
using Xunit;
using Xunit.Abstractions;
using System.Net.Http;
using System.Net.Http.Headers;
using BTCPayServer.Data;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenQA.Selenium;
using Microsoft.AspNetCore.Identity;
namespace BTCPayServer.Tests
{
public class AuthenticationTests
{
public const string TestApiPath = "api/test/openid";
public const int TestTimeout = TestUtils.TestTimeout;
public AuthenticationTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task GetRedirectedToLoginPathOnChallenge()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var client = tester.PayTester.HttpClient;
//Wallets endpoint is protected
var response = await client.GetAsync("wallets");
var urlPath = response.RequestMessage.RequestUri.ToString()
.Replace(tester.PayTester.ServerUri.ToString(), "");
//Cookie Challenge redirects you to login page
Assert.StartsWith("Account/Login", urlPath, StringComparison.InvariantCultureIgnoreCase);
var queryString = response.RequestMessage.RequestUri.ParseQueryString();
Assert.NotNull(queryString["ReturnUrl"]);
Assert.Equal("/wallets", queryString["ReturnUrl"]);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanGetOpenIdConfiguration()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
using (var response =
await tester.PayTester.HttpClient.GetAsync("/.well-known/openid-configuration"))
{
using (var streamToReadFrom = new StreamReader(await response.Content.ReadAsStreamAsync()))
{
var json = await streamToReadFrom.ReadToEndAsync();
Assert.NotNull(json);
JObject.Parse(json); // Should do more tests but good enough
}
}
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUseNonInteractiveFlows()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var token = await RegisterPasswordClientAndGetAccessToken(user, null, tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterPasswordClientAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
token = await RegisterClientCredentialsFlowAndGetAccessToken(user, "secret", tester);
await TestApiAgainstAccessToken(token, tester, user);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Selenium", "Selenium")]
public async Task CanUseImplicitFlow()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Implicit},
RedirectUris = {redirecturi},
});
var implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid server_management store_management&nonce={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.Driver.FindElement(By.Id("consent-yes")).Click();
var url = s.Driver.Url;
var results = url.Split("#").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
//in Implicit mode, you renew your token by hitting the same endpoint but adding prompt=none. If you are still logged in on the site, you will receive a fresh token.
var implicitAuthorizeUrlSilentModel = new Uri($"{implicitAuthorizeUrl.OriginalString}&prompt=none");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrlSilentModel);
url = s.Driver.Url;
results = url.Split("#").Last().Split("&").ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await TestApiAgainstAccessToken(results["access_token"], tester, user);
var stores = await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
$"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
Assert.NotEmpty(stores);
Assert.True(await TestApiAgainstAccessToken<bool>(results["access_token"],
$"{TestApiPath}/me/stores/{stores[0].Id}/can-edit",
tester.PayTester.HttpClient));
//we dont ask for consent after acquiring it the first time for the same scopes.
LogoutFlow(tester, id, s);
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.Driver.AssertElementNotFound(By.Id("consent-yes"));
// Let's asks without scopes
LogoutFlow(tester, id, s);
id = Guid.NewGuid().ToString();
openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = { OpenIddictConstants.Permissions.GrantTypes.Implicit },
RedirectUris = { redirecturi },
});
implicitAuthorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=token&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid&nonce={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.Driver.FindElement(By.Id("consent-yes")).Click();
results = s.Driver.Url.Split("#").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<StoreData[]>(results["access_token"],
$"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
});
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(results["access_token"],
$"{TestApiPath}/me/stores/{stores[0].Id}/can-edit",
tester.PayTester.HttpClient);
});
}
}
void LogoutFlow(ServerTester tester, string clientId, SeleniumTester seleniumTester)
{
var logoutUrl = new Uri(tester.PayTester.ServerUri,
$"connect/logout?response_type=token&client_id={clientId}");
seleniumTester.Driver.Navigate().GoToUrl(logoutUrl);
seleniumTester.GoToHome();
Assert.Throws<NoSuchElementException>(() => seleniumTester.Driver.FindElement(By.Id("Logout")));
}
[Fact(Timeout = TestTimeout)]
[Trait("Selenium", "Selenium")]
public async Task CanUseCodeFlow()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
var tester = s.Server;
var user = tester.NewAccount();
user.GrantAccess();
await user.MakeAdmin();
var id = Guid.NewGuid().ToString();
var redirecturi = new Uri("http://127.0.0.1/oidc-callback");
var secret = "secret";
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions =
{
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken
},
RedirectUris = {redirecturi}
}, secret);
var authorizeUrl = new Uri(tester.PayTester.ServerUri,
$"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access server_management store_management&state={Guid.NewGuid().ToString()}");
s.Driver.Navigate().GoToUrl(authorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
s.Driver.FindElement(By.Id("consent-yes")).Click();
var url = s.Driver.Url;
var results = url.Split("?").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.AuthorizationCode),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret),
new KeyValuePair<string, string>("code", results["code"]),
new KeyValuePair<string, string>("redirect_uri", redirecturi.AbsoluteUri)
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
await TestApiAgainstAccessToken(result.AccessToken, tester, user);
var refreshedAccessToken = await RefreshAnAccessToken(result.RefreshToken, httpClient, id, secret);
await TestApiAgainstAccessToken(refreshedAccessToken, tester, user);
LogoutFlow(tester, id, s);
s.Driver.Navigate().GoToUrl(authorizeUrl);
s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password);
Assert.Throws<NoSuchElementException>(() => s.Driver.FindElement(By.Id("consent-yes")));
results = url.Split("?").Last().Split("&")
.ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]);
Assert.True(results.ContainsKey("code"));
}
}
private static async Task<string> RefreshAnAccessToken(string refreshToken, HttpClient client, string clientId,
string clientSecret = null)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(client.BaseAddress, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.RefreshToken),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("refresh_token", refreshToken)
})
};
var response = await client.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterClientCredentialsFlowAndGetAccessToken(TestAccount user,
string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.ClientCredentials}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type",
OpenIddictConstants.GrantTypes.ClientCredentials),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret),
new KeyValuePair<string, string>("scope", "server_management store_management")
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
private static async Task<string> RegisterPasswordClientAndGetAccessToken(TestAccount user, string secret,
ServerTester tester)
{
var id = Guid.NewGuid().ToString();
var openIdClient = await user.RegisterOpenIdClient(
new OpenIddictApplicationDescriptor()
{
ClientId = id,
DisplayName = id,
Permissions = {OpenIddictConstants.Permissions.GrantTypes.Password}
}, secret);
var httpClient = tester.PayTester.HttpClient;
var httpRequest = new HttpRequestMessage(HttpMethod.Post,
new Uri(tester.PayTester.ServerUri, "/connect/token"))
{
Content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", OpenIddictConstants.GrantTypes.Password),
new KeyValuePair<string, string>("username", user.RegisterDetails.Email),
new KeyValuePair<string, string>("password", user.RegisterDetails.Password),
new KeyValuePair<string, string>("client_id", openIdClient.ClientId),
new KeyValuePair<string, string>("client_secret", secret),
new KeyValuePair<string, string>("scope", "server_management store_management")
})
};
var response = await httpClient.SendAsync(httpRequest);
Assert.True(response.IsSuccessStatusCode);
string content = await response.Content.ReadAsStringAsync();
var result = System.Text.Json.JsonSerializer.Deserialize<OpenIddictResponse>(content);
Assert.NotEmpty(result.AccessToken);
Assert.Null(result.Error);
return result.AccessToken;
}
async Task TestApiAgainstAccessToken(string accessToken, ServerTester tester, TestAccount testAccount)
{
var resultUser =
await TestApiAgainstAccessToken<string>(accessToken, $"{TestApiPath}/me/id",
tester.PayTester.HttpClient);
Assert.Equal(testAccount.UserId, resultUser);
var secondUser = tester.NewAccount();
secondUser.GrantAccess();
var resultStores =
await TestApiAgainstAccessToken<StoreData[]>(accessToken, $"{TestApiPath}/me/stores",
tester.PayTester.HttpClient);
Assert.Contains(resultStores,
data => data.Id.Equals(testAccount.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.DoesNotContain(resultStores,
data => data.Id.Equals(secondUser.StoreId, StringComparison.InvariantCultureIgnoreCase));
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/stores/{testAccount.StoreId}/can-edit",
tester.PayTester.HttpClient));
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
$"{TestApiPath}/me/is-admin",
tester.PayTester.HttpClient));
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
{
await TestApiAgainstAccessToken<bool>(accessToken, $"{TestApiPath}/me/stores/{secondUser.StoreId}/can-edit",
tester.PayTester.HttpClient);
});
}
public async Task<T> TestApiAgainstAccessToken<T>(string accessToken, string url, HttpClient client)
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get,
new Uri(client.BaseAddress, url));
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var result = await client.SendAsync(httpRequest);
result.EnsureSuccessStatusCode();
var rawJson = await result.Content.ReadAsStringAsync();
if (typeof(T).IsPrimitive || typeof(T) == typeof(string))
{
return (T)Convert.ChangeType(rawJson, typeof(T));
}
return JsonConvert.DeserializeObject<T>(rawJson);
}
}
}

View file

@ -32,7 +32,6 @@ using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using OpenIddict.Abstractions;
using Xunit; using Xunit;
using BTCPayServer.Services; using BTCPayServer.Services;
using System.Net.Http; using System.Net.Http;
@ -298,7 +297,7 @@ namespace BTCPayServer.Tests
if (userId != null) if (userId != null)
{ {
List<Claim> claims = new List<Claim>(); List<Claim> claims = new List<Claim>();
claims.Add(new Claim(OpenIddictConstants.Claims.Subject, userId)); claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (isAdmin) if (isAdmin)
claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin)); claims.Add(new Claim(ClaimTypes.Role, Roles.ServerAdmin));
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie)); context.User = new ClaimsPrincipal(new ClaimsIdentity(claims.ToArray(), AuthenticationSchemes.Cookie));

View file

@ -18,8 +18,6 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning; using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning; using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Data; using BTCPayServer.Data;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using NBXplorer.Models; using NBXplorer.Models;
@ -194,14 +192,5 @@ namespace BTCPayServer.Tests
if (storeController.ModelState.ErrorCount != 0) if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage); Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
} }
public async Task<BTCPayOpenIdClient> RegisterOpenIdClient(OpenIddictApplicationDescriptor descriptor, string secret = null)
{
var openIddictApplicationManager = parent.PayTester.GetService<OpenIddictApplicationManager<BTCPayOpenIdClient>>();
var client = new BTCPayOpenIdClient { Id = Guid.NewGuid().ToString(), ApplicationUserId = UserId};
await openIddictApplicationManager.PopulateAsync(client, descriptor);
await openIddictApplicationManager.CreateAsync(client, secret);
return client;
}
} }
} }

View file

@ -66,9 +66,6 @@
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" /> <PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
<PackageReference Include="U2F.Core" Version="1.0.4" /> <PackageReference Include="U2F.Core" Version="1.0.4" />
<PackageReference Include="YamlDotNet" Version="8.0.0" /> <PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
</ItemGroup> </ItemGroup>
@ -220,4 +217,8 @@
<Pack>$(IncludeRazorContentInPack)</Pack> <Pack>$(IncludeRazorContentInPack)</Pack>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
</ItemGroup>
</Project> </Project>

View file

@ -1,136 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Security.OpenId;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.Authorization;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.Server;
using System.Security.Claims;
using OpenIddict.Server.AspNetCore;
namespace BTCPayServer.Controllers
{
public class AuthorizationController : Controller
{
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> _authorizationManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<IdentityOptions> _IdentityOptions;
public AuthorizationController(
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
SignInManager<ApplicationUser> signInManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> identityOptions)
{
_applicationManager = applicationManager;
_signInManager = signInManager;
_authorizationManager = authorizationManager;
_userManager = userManager;
_IdentityOptions = identityOptions;
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest();
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
return View("Error",
new ErrorViewModel
{
Error = OpenIddictConstants.Errors.InvalidClient,
ErrorDescription =
"Details concerning the calling client application cannot be found in the database"
});
}
var userId = _userManager.GetUserId(User);
if (!string.IsNullOrEmpty(
await OpenIdExtensions.IsUserAuthorized(_authorizationManager, request, userId, application.Id)))
{
return await Authorize("YES", false);
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
RequestId = request.RequestId,
Scope = request.GetScopes()
});
}
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("/connect/authorize")]
public async Task<IActionResult> Authorize(string consent, bool createAuthorization = true)
{
var request = HttpContext.GetOpenIddictServerRequest();
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return View("Error",
new ErrorViewModel
{
Error = OpenIddictConstants.Errors.ServerError,
ErrorDescription = "The specified user could not be found"
});
}
string type = null;
switch (consent.ToUpperInvariant())
{
case "YESTEMPORARY":
type = OpenIddictConstants.AuthorizationTypes.AdHoc;
break;
case "YES":
type = OpenIddictConstants.AuthorizationTypes.Permanent;
break;
case "NO":
default:
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
principal = await _signInManager.CreateUserPrincipalAsync(user);
principal.SetScopes(request.GetScopes().Restrict(principal));
principal.SetDestinations(_IdentityOptions.Value);
if (createAuthorization)
{
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
var authorization = await _authorizationManager.CreateAsync(User, user.Id, application.Id,
type, principal.GetScopes());
principal.SetInternalAuthorizationId(authorization.Id);
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
}

View file

@ -1,65 +0,0 @@
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Validation.AspNetCore;
namespace BTCPayServer.Controllers.RestApi
{
/// <summary>
/// this controller serves as a testing endpoint for our OpenId unit tests
/// </summary>
[Route("api/test/openid")]
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.OpenId)]
public class TestOpenIdController : ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
public TestOpenIdController(UserManager<ApplicationUser> userManager, StoreRepository storeRepository)
{
_userManager = userManager;
_storeRepository = storeRepository;
}
[HttpGet("me/id")]
public string GetCurrentUserId()
{
return _userManager.GetUserId(User);
}
[HttpGet("me")]
public async Task<ApplicationUser> GetCurrentUser()
{
return await _userManager.GetUserAsync(User);
}
[HttpGet("me/is-admin")]
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = AuthenticationSchemes.OpenId)]
public bool AmIAnAdmin()
{
return true;
}
[HttpGet("me/stores")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
public async Task<StoreData[]> GetCurrentUserStores()
{
return await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
}
[HttpGet("me/stores/{storeId}/can-edit")]
[Authorize(Policy = Policies.CanModifyStoreSettings.Key,
AuthenticationSchemes = AuthenticationSchemes.OpenId)]
public bool CanEdit(string storeId)
{
return true;
}
}
}

View file

@ -1,37 +0,0 @@
using System.IO;
using System.Security.Cryptography;
using BTCPayServer.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using NETCore.Encrypt.Extensions.Internal;
namespace BTCPayServer
{
public static class OpenIddictExtensions
{
public static SecurityKey GetSigningKey(IConfiguration configuration, string fileName)
{
var file = Path.Combine(configuration.GetDataDir(), fileName);
using var rsa = new RSACryptoServiceProvider(2048);
if (File.Exists(file))
{
rsa.FromXmlString2(File.ReadAllText(file));
}
else
{
var contents = rsa.ToXmlString2(true);
File.WriteAllText(file, contents);
}
return new RsaSecurityKey(rsa.ExportParameters(true));;
}
public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder,
IConfiguration configuration)
{
return builder
.AddSigningKey(GetSigningKey(configuration, "signing.rsaparams"))
.AddEncryptionKey(GetSigningKey(configuration, "encrypting.rsaparams"));
}
}
}

View file

@ -1,97 +0,0 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Xml;
namespace NETCore.Encrypt.Extensions.Internal
{
/// <summary>
/// .net core's implementatiosn are still marked as unsupported because of stupid decisions( https://github.com/dotnet/corefx/issues/23686)
/// </summary>
internal static class RsaKeyExtensions
{
#region XML
public static void FromXmlString2(this RSA rsa, string xmlString)
{
RSAParameters parameters = new RSAParameters();
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xmlString);
if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue", StringComparison.InvariantCulture))
{
foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes)
{
switch (node.Name)
{
case "Modulus":
parameters.Modulus = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "Exponent":
parameters.Exponent = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "P":
parameters.P = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "Q":
parameters.Q = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "DP":
parameters.DP = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "DQ":
parameters.DQ = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "InverseQ":
parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
case "D":
parameters.D = (string.IsNullOrEmpty(node.InnerText)
? null
: Convert.FromBase64String(node.InnerText));
break;
}
}
}
else
{
throw new Exception("Invalid XML RSA key.");
}
rsa.ImportParameters(parameters);
}
public static string ToXmlString2(this RSA rsa, bool includePrivateParameters)
{
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters);
return string.Format(CultureInfo.InvariantCulture,
"<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null,
parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null,
parameters.P != null ? Convert.ToBase64String(parameters.P) : null,
parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null,
parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null,
parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null,
parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null,
parameters.D != null ? Convert.ToBase64String(parameters.D) : null);
}
#endregion
}
}

View file

@ -41,17 +41,6 @@ using Npgsql;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.U2F; using BTCPayServer.U2F;
using BundlerMinifier.TagHelpers; using BundlerMinifier.TagHelpers;
using OpenIddict.EntityFrameworkCore.Models;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Security.Bitpay; using BTCPayServer.Security.Bitpay;
using Serilog; using Serilog;
@ -67,7 +56,6 @@ namespace BTCPayServer.Hosting
{ {
var factory = provider.GetRequiredService<ApplicationDbContextFactory>(); var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o); factory.ConfigureBuilder(o);
o.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
}); });
services.AddHttpClient(); services.AddHttpClient();
services.AddHttpClient(nameof(ExplorerClientProvider), httpClient => services.AddHttpClient(nameof(ExplorerClientProvider), httpClient =>
@ -221,7 +209,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>(); services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>(); services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>(); services.AddScoped<IAuthorizationHandler, BitpayAuthorizationHandler>();
services.TryAddSingleton<ExplorerClientProvider>(); services.TryAddSingleton<ExplorerClientProvider>();

View file

@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using OpenIddict.Validation.AspNetCore;
using OpenIddict.Abstractions;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -19,14 +18,10 @@ using System.IO;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using BTCPayServer.Security; using BTCPayServer.Security;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using OpenIddict.EntityFrameworkCore.Models;
using System.Net; using System.Net;
using BTCPayServer.Security.OpenId;
using BTCPayServer.PaymentRequest; using BTCPayServer.PaymentRequest;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Storage; using BTCPayServer.Storage;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@ -57,8 +52,6 @@ namespace BTCPayServer.Hosting
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
ConfigureOpenIddict(services);
services.AddBTCPayServer(Configuration); services.AddBTCPayServer(Configuration);
services.AddProviderStorage(); services.AddProviderStorage();
services.AddSession(); services.AddSession();
@ -95,12 +88,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true; options.Lockout.AllowedForNewUsers = true;
options.Password.RequireUppercase = false; options.Password.RequireUppercase = false;
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
options.ClaimsIdentity.UserNameClaimType = OpenIddictConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIddictConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIddictConstants.Claims.Role;
}); });
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be. // If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null); string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
@ -143,67 +130,6 @@ namespace BTCPayServer.Hosting
}); });
} }
} }
private DirectoryInfo GetDataDir()
{
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
}
private void ConfigureOpenIddict(IServiceCollection services)
{
// Register the OpenIddict services.
services.AddOpenIddict()
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and entities.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>()
.ReplaceDefaultEntities<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>,
BTCPayOpenIdToken, string>();
})
.AddServer(options =>
{
options.UseAspNetCore()
.EnableStatusCodePagesIntegration()
.EnableAuthorizationEndpointPassthrough()
.EnableLogoutEndpointPassthrough()
.EnableAuthorizationEndpointCaching()
.DisableTransportSecurityRequirement();
// Enable the token endpoint (required to use the password flow).
options.SetTokenEndpointUris("/connect/token");
options.SetAuthorizationEndpointUris("/connect/authorize");
options.SetLogoutEndpointUris("/connect/logout");
//we do not care about these granular controls for now
options.IgnoreScopePermissions();
options.IgnoreEndpointPermissions();
// Allow client applications various flows
options.AllowImplicitFlow();
options.AllowClientCredentialsFlow();
options.AllowRefreshTokenFlow();
options.AllowPasswordFlow();
options.AllowAuthorizationCodeFlow();
options.UseRollingTokens();
options.RegisterScopes(
OpenIddictConstants.Scopes.OpenId,
BTCPayScopes.StoreManagement,
BTCPayScopes.ServerManagement
);
options.AddEventHandler(PasswordGrantTypeEventHandler.Descriptor);
options.AddEventHandler(OpenIdGrantHandlerCheckCanSignIn.Descriptor);
options.AddEventHandler(ClientCredentialsGrantTypeEventHandler.Descriptor);
options.AddEventHandler(LogoutEventHandler.Descriptor);
options.ConfigureSigningKey(Configuration);
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
}
public void Configure( public void Configure(
IApplicationBuilder app, IApplicationBuilder app,
IWebHostEnvironment env, IWebHostEnvironment env,
@ -224,6 +150,10 @@ namespace BTCPayServer.Hosting
}); });
} }
} }
private DirectoryInfo GetDataDir()
{
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
}
private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options) private static void ConfigureCore(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider prov, ILoggerFactory loggerFactory, BTCPayServerOptions options)
{ {

View file

@ -53,7 +53,6 @@ namespace BTCPayServer
l.AddFilter("Microsoft", LogLevel.Error); l.AddFilter("Microsoft", LogLevel.Error);
l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical); l.AddFilter("System.Net.Http.HttpClient", LogLevel.Critical);
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical); l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddFilter("OpenIddict.Server.OpenIddictServerProvider", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider(processor)); l.AddProvider(new CustomConsoleLogProvider(processor));
}) })
.UseStartup<Startup>() .UseStartup<Startup>()

View file

@ -1,17 +1,9 @@
using System; namespace BTCPayServer.Security
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using OpenIddict.Validation;
using OpenIddict.Validation.AspNetCore;
namespace BTCPayServer.Security
{ {
public class AuthenticationSchemes public class AuthenticationSchemes
{ {
public const string Cookie = "Identity.Application"; public const string Cookie = "Identity.Application";
public const string Bitpay = "Bitpay"; public const string Bitpay = "Bitpay";
public const string OpenId = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
public const string ApiKey = "GreenfieldApiKey"; public const string ApiKey = "GreenfieldApiKey";
} }
} }

View file

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using OpenIddict.Abstractions;
namespace BTCPayServer.Security.OpenId
{
public static class BTCPayScopes
{
public const string StoreManagement = "store_management";
public const string ServerManagement = "server_management";
}
public static class RestAPIPolicies
{
}
}

View file

@ -1,43 +0,0 @@
using System.Threading.Tasks;
using OpenIdConnectRequest = OpenIddict.Abstractions.OpenIddictRequest;
using BTCPayServer.Data;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
using OpenIddict.Server;
using System.Security.Claims;
namespace BTCPayServer.Security.OpenId
{
public abstract class BaseOpenIdGrantHandler<T> :
IOpenIddictServerHandler<T>
where T : OpenIddictServerEvents.BaseContext
{
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
private readonly OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> _authorizationManager;
protected readonly SignInManager<ApplicationUser> _signInManager;
protected readonly IOptions<IdentityOptions> _identityOptions;
protected BaseOpenIdGrantHandler(
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_signInManager = signInManager;
_identityOptions = identityOptions;
}
protected Task<ClaimsPrincipal> CreateClaimsPrincipalAsync(OpenIdConnectRequest request, ApplicationUser user)
{
return OpenIdExtensions.CreateClaimsPrincipalAsync(_applicationManager, _authorizationManager,
_identityOptions.Value, _signInManager, request, user);
}
public abstract ValueTask HandleAsync(T notification);
}
}

View file

@ -1,61 +0,0 @@
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Server;
namespace BTCPayServer.Security.OpenId
{
public class ClientCredentialsGrantTypeEventHandler :
BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
{
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
.UseScopedHandler<ClientCredentialsGrantTypeEventHandler>()
.Build();
private readonly OpenIddictApplicationManager<BTCPayOpenIdClient> _applicationManager;
private readonly UserManager<ApplicationUser> _userManager;
public ClientCredentialsGrantTypeEventHandler(
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions,
UserManager<ApplicationUser> userManager) : base(applicationManager, authorizationManager, signInManager,
identityOptions)
{
_applicationManager = applicationManager;
_userManager = userManager;
}
public override async ValueTask HandleAsync(
OpenIddictServerEvents.HandleTokenRequestContext notification)
{
var request = notification.Request;
var context = notification;
if (!request.IsClientCredentialsGrantType())
{
return;
}
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidClient,
description: "The client application was not found in the database.");
return;
}
var user = await _userManager.FindByIdAsync(application.ApplicationUserId);
context.Principal = await CreateClaimsPrincipalAsync(request, user);
}
}
}

View file

@ -1,34 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
using OpenIddict.Server;
using Microsoft.AspNetCore;
using OpenIddict.Server.AspNetCore;
namespace BTCPayServer.Security.OpenId
{
public class LogoutEventHandler : IOpenIddictServerHandler<OpenIddictServerEvents.HandleLogoutRequestContext>
{
protected readonly SignInManager<ApplicationUser> _signInManager;
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleLogoutRequestContext>()
.UseScopedHandler<LogoutEventHandler>()
.Build();
public LogoutEventHandler(SignInManager<ApplicationUser> signInManager)
{
_signInManager = signInManager;
}
public async ValueTask HandleAsync(
OpenIddictServerEvents.HandleLogoutRequestContext notification)
{
await _signInManager.SignOutAsync();
}
}
}

View file

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.Server;
namespace BTCPayServer.Security.OpenId
{
public static class OpenIdExtensions
{
public static ImmutableHashSet<string> Restrict(this ImmutableArray<string> scopes, ClaimsPrincipal claimsPrincipal)
{
HashSet<string> restricted = new HashSet<string>();
foreach (var scope in scopes)
{
if (scope == BTCPayScopes.ServerManagement && !claimsPrincipal.IsInRole(Roles.ServerAdmin))
continue;
restricted.Add(scope);
}
return restricted.ToImmutableHashSet();
}
public static async Task<ClaimsPrincipal> CreateClaimsPrincipalAsync(OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
IdentityOptions identityOptions,
SignInManager<ApplicationUser> signInManager,
OpenIddictRequest request,
ApplicationUser user)
{
var principal = await signInManager.CreateUserPrincipalAsync(user);
if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType())
{
principal.SetScopes(request.GetScopes().Restrict(principal));
}
else if (request.IsAuthorizationCodeGrantType() &&
string.IsNullOrEmpty(principal.GetInternalAuthorizationId()))
{
var app = await applicationManager.FindByClientIdAsync(request.ClientId);
var authorizationId = await IsUserAuthorized(authorizationManager, request, user.Id, app.Id);
if (!string.IsNullOrEmpty(authorizationId))
{
principal.SetInternalAuthorizationId(authorizationId);
}
}
principal.SetDestinations(identityOptions);
return principal;
}
public static void SetDestinations(this ClaimsPrincipal principal, IdentityOptions identityOptions)
{
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(identityOptions, claim, principal));
}
}
private static IEnumerable<string> GetDestinations(IdentityOptions identityOptions, Claim claim,
ClaimsPrincipal principal)
{
switch (claim.Type)
{
case OpenIddictConstants.Claims.Name:
case OpenIddictConstants.Claims.Email:
yield return OpenIddictConstants.Destinations.AccessToken;
yield break;
}
}
public static async Task<string> IsUserAuthorized(
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
OpenIddictRequest request, string userId, string applicationId)
{
var authorizations = await authorizationManager.ListAsync(queryable =>
queryable.Where(authorization =>
authorization.Subject == userId &&
authorization.Application.Id == applicationId &&
authorization.Status == OpenIddictConstants.Statuses.Valid)).ToArrayAsync();
if (authorizations.Length > 0)
{
var scopeTasks = authorizations.Select(authorization =>
(authorizationManager.GetScopesAsync(authorization).AsTask(), authorization.Id));
await Task.WhenAll(scopeTasks.Select((tuple) => tuple.Item1));
var authorizationsWithSufficientScopes = scopeTasks
.Select((tuple) => (Id: tuple.Id, Scopes: tuple.Item1.Result))
.Where((tuple) => !request.GetScopes().Except(tuple.Scopes).Any());
if (authorizationsWithSufficientScopes.Any())
{
return authorizationsWithSufficientScopes.First().Id;
}
}
return null;
}
}
}

View file

@ -1,70 +0,0 @@
using System.Threading.Tasks;
using OpenIddict.Abstractions;
using BTCPayServer.Data;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Core;
using OpenIddict.Server;
using Microsoft.AspNetCore;
using OpenIddict.Server.AspNetCore;
namespace BTCPayServer.Security.OpenId
{
public class OpenIdGrantHandlerCheckCanSignIn :
BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
{
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
.UseScopedHandler<OpenIdGrantHandlerCheckCanSignIn>()
.Build();
private readonly UserManager<ApplicationUser> _userManager;
public OpenIdGrantHandlerCheckCanSignIn(
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityOptions> identityOptions, UserManager<ApplicationUser> userManager) : base(
applicationManager, authorizationManager, signInManager,
identityOptions)
{
_userManager = userManager;
}
public override async ValueTask HandleAsync(
OpenIddictServerEvents.HandleTokenRequestContext notification)
{
var request = notification.Request;
if (!request.IsRefreshTokenGrantType() && !request.IsAuthorizationCodeGrantType())
{
// Allow other handlers to process the event.
return;
}
var httpContext = notification.Transaction.GetHttpRequest().HttpContext;
var authenticateResult = (await httpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
var user = await _userManager.GetUserAsync(authenticateResult.Principal);
if (user == null)
{
notification.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The token is no longer valid.");
return;
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
notification.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The user is no longer allowed to sign in.");
return;
}
notification.Principal = await this.CreateClaimsPrincipalAsync(request, user);
}
}
}

View file

@ -1,65 +0,0 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.U2F;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.Server;
using Microsoft.AspNetCore;
namespace BTCPayServer.Security.OpenId
{
public class PasswordGrantTypeEventHandler : BaseOpenIdGrantHandler<OpenIddictServerEvents.HandleTokenRequestContext>
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly NicolasDorier.RateLimits.RateLimitService _rateLimitService;
private readonly U2FService _u2FService;
public PasswordGrantTypeEventHandler(
OpenIddictApplicationManager<BTCPayOpenIdClient> applicationManager,
OpenIddictAuthorizationManager<BTCPayOpenIdAuthorization> authorizationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
NicolasDorier.RateLimits.RateLimitService rateLimitService,
IOptions<IdentityOptions> identityOptions, U2FService u2FService) : base(applicationManager,
authorizationManager, signInManager, identityOptions)
{
_userManager = userManager;
_rateLimitService = rateLimitService;
_u2FService = u2FService;
}
public static OpenIddictServerHandlerDescriptor Descriptor { get; } =
OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.HandleTokenRequestContext>()
.UseScopedHandler<PasswordGrantTypeEventHandler>()
.Build();
public override async ValueTask HandleAsync(
OpenIddictServerEvents.HandleTokenRequestContext notification)
{
var request = notification.Request;
if (!request.IsPasswordGrantType())
{
return;
}
var httpContext = notification.Transaction.GetHttpRequest().HttpContext;
await _rateLimitService.Throttle(ZoneLimits.Login, httpContext.Connection.RemoteIpAddress.ToString(), httpContext.RequestAborted);
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || await _u2FService.HasDevices(user.Id) ||
!(await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true))
.Succeeded)
{
notification.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: "The specified credentials are invalid.");
return;
}
notification.Principal = await CreateClaimsPrincipalAsync(request, user);
}
}
}

View file

@ -1,83 +0,0 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Primitives;
using static BTCPayServer.Security.OpenId.RestAPIPolicies;
using OpenIddict.Abstractions;
using BTCPayServer.Security.OpenId;
namespace BTCPayServer.Security
{
public class OpenIdAuthorizationHandler : AuthorizationHandler<PolicyRequirement>
{
private readonly HttpContext _HttpContext;
private readonly UserManager<ApplicationUser> _userManager;
private readonly StoreRepository _storeRepository;
public OpenIdAuthorizationHandler(IHttpContextAccessor httpContextAccessor,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository)
{
_HttpContext = httpContextAccessor.HttpContext;
_userManager = userManager;
_storeRepository = storeRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
{
if (context.User.Identity.AuthenticationType != "AuthenticationTypes.Federation")
return;
bool success = false;
switch (requirement.Policy)
{
case Policies.CanModifyStoreSettings.Key:
if (!context.HasScopes(BTCPayScopes.StoreManagement))
break;
// TODO: It should be possible to grant permission to a specific store
// we can do this by adding saving a claim with the specific store id
// to the access_token
string storeId = _HttpContext.GetImplicitStoreId();
if (storeId == null)
{
success = true;
}
else
{
var userid = _userManager.GetUserId(context.User);
if (string.IsNullOrEmpty(userid))
break;
var store = await _storeRepository.FindStore((string)storeId, userid);
if (store == null)
break;
success = true;
_HttpContext.SetStoreData(store);
}
break;
case Policies.CanModifyServerSettings.Key:
if (!context.HasScopes(BTCPayScopes.ServerManagement))
break;
// For this authorization, we stil check in database because it is super sensitive.
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
break;
if (!await _userManager.IsInRoleAsync(user, Roles.ServerAdmin))
break;
success = true;
break;
}
if (success)
{
context.Succeed(requirement);
}
}
}
}

View file

@ -1,14 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using OpenIddict.Abstractions;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
namespace BTCPayServer.Security namespace BTCPayServer.Security
{ {
@ -16,7 +10,7 @@ namespace BTCPayServer.Security
{ {
public static bool HasScopes(this AuthorizationHandlerContext context, params string[] scopes) public static bool HasScopes(this AuthorizationHandlerContext context, params string[] scopes)
{ {
return scopes.All(s => context.User.HasClaim(c => c.Type == OpenIddictConstants.Claims.Scope && c.Value.Split(' ').Contains(s))); return scopes.All(s => context.User.HasClaim(c => c.Type.Equals("scope", StringComparison.InvariantCultureIgnoreCase) && c.Value.Split(' ').Contains(s)));
} }
public static string GetImplicitStoreId(this HttpContext httpContext) public static string GetImplicitStoreId(this HttpContext httpContext)
{ {

View file

@ -1,57 +0,0 @@
@using BTCPayServer.Security.OpenId
@using OpenIddict.Abstractions
@model BTCPayServer.Models.Authorization.AuthorizeViewModel
@{
var scopeMappings = new Dictionary<string, (string Title, string Description)>()
{
{BTCPayScopes.StoreManagement, ("Manage your stores", "The app will be able to create, modify and delete all your stores.")},
{BTCPayScopes.ServerManagement, ("Manage your server", "The app will have total control on your server")},
};
}
<form method="post">
<input type="hidden" name="request_id" value="@Model.RequestId"/>
<section>
<div class="card container">
<div class="row">
<div class="col-lg-12 section-heading">
<h2>Authorization Request</h2>
<hr class="primary">
<p>@Model.ApplicationName is requesting access to your account.</p>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="list-group list-group-flush">
@foreach (var scope in Model.Scope)
{
@if (scopeMappings.TryGetValue(scope, out var text))
{
<li class="list-group-item">
<h5 class="mb-1">@text.Title</h5>
<p class="mb-1">@text.Description.</p>
</li>
}
}
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-lg-12 text-center">
<div class="btn-group">
<button class="btn btn-primary" name="consent" id="consent-yes" type="submit" value="Yes">Authorize app</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<button class="dropdown-item" name="consent" id="consent-yes-temporary" type="submit" value="YesTemporary">Authorize app until session ends</button>
</div>
</div>
<button class="btn btn-secondary" id="consent-no" name="consent" type="submit" value="No">Cancel</button>
</div>
</div>
</div>
</section>
</form>