From c5227d9996fb5cb962b000079e9bb75f8228f391 Mon Sep 17 00:00:00 2001 From: Andrew Camilleri Date: Thu, 29 Aug 2019 09:25:16 +0200 Subject: [PATCH] Request consent from user before giving application access to the user's data & services. --- BTCPayServer.Tests/AuthenticationTests.cs | 23 ++- .../AuthorizationCodeGrantTypeEventHandler.cs | 12 +- .../OpenId/AuthorizationEventHandler.cs | 78 ---------- .../OpenId/BaseOpenIdGrantHandler.cs | 86 ++--------- .../ClientCredentialsGrantTypeEventHandler.cs | 7 +- .../OpenId/LogoutEventHandler.cs | 15 +- .../Authentication/OpenId/OpenIdExtensions.cs | 139 ++++++++++++++++++ .../OpenIdGrantHandlerCheckCanSignIn.cs | 11 +- .../OpenId/PasswordGrantTypeEventHandler.cs | 12 +- .../RefreshTokenGrantTypeEventHandler.cs | 12 +- .../Controllers/AuthorizationController.cs | 136 +++++++++++++++++ .../Extensions/OpenIddictExtensions.cs | 19 +-- BTCPayServer/Hosting/Startup.cs | 3 +- .../Authorization/AuthorizeViewModel.cs | 14 ++ .../Views/Authorization/Authorize.cshtml | 31 ++++ 15 files changed, 410 insertions(+), 188 deletions(-) delete mode 100644 BTCPayServer/Authentication/OpenId/AuthorizationEventHandler.cs create mode 100644 BTCPayServer/Authentication/OpenId/OpenIdExtensions.cs create mode 100644 BTCPayServer/Controllers/AuthorizationController.cs create mode 100644 BTCPayServer/Models/Authorization/AuthorizeViewModel.cs create mode 100644 BTCPayServer/Views/Authorization/Authorize.cshtml diff --git a/BTCPayServer.Tests/AuthenticationTests.cs b/BTCPayServer.Tests/AuthenticationTests.cs index 22acbb679..9b0ace868 100644 --- a/BTCPayServer.Tests/AuthenticationTests.cs +++ b/BTCPayServer.Tests/AuthenticationTests.cs @@ -114,20 +114,27 @@ namespace BTCPayServer.Tests $"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(); 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(implicitAuthorizeUrl); + 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); LogoutFlow(tester, id, s); + + s.Driver.Navigate().GoToUrl(implicitAuthorizeUrl); + s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); + + Assert.Throws(() => s.Driver.FindElement(By.Id("consent-yes"))); + results = url.Split("#").Last().Split("&") + .ToDictionary(s1 => s1.Split("=")[0], s1 => s1.Split("=")[1]); + await TestApiAgainstAccessToken(results["access_token"], tester, user); } } @@ -171,6 +178,7 @@ namespace BTCPayServer.Tests $"connect/authorize?response_type=code&client_id={id}&redirect_uri={redirecturi.AbsoluteUri}&scope=openid offline_access&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]); @@ -204,6 +212,15 @@ namespace BTCPayServer.Tests 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(() => 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")); } } diff --git a/BTCPayServer/Authentication/OpenId/AuthorizationCodeGrantTypeEventHandler.cs b/BTCPayServer/Authentication/OpenId/AuthorizationCodeGrantTypeEventHandler.cs index c83ac835f..91eed8966 100644 --- a/BTCPayServer/Authentication/OpenId/AuthorizationCodeGrantTypeEventHandler.cs +++ b/BTCPayServer/Authentication/OpenId/AuthorizationCodeGrantTypeEventHandler.cs @@ -1,14 +1,20 @@ using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using OpenIddict.Core; namespace BTCPayServer.Authentication.OpenId { public class AuthorizationCodeGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn { - public AuthorizationCodeGrantTypeEventHandler(SignInManager signInManager, - IOptions identityOptions, UserManager userManager) : base(signInManager, + public AuthorizationCodeGrantTypeEventHandler( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, + IOptions identityOptions, + UserManager userManager) : base(applicationManager, authorizationManager, signInManager, identityOptions, userManager) { } @@ -18,4 +24,4 @@ namespace BTCPayServer.Authentication.OpenId return request.IsAuthorizationCodeGrantType(); } } -} \ No newline at end of file +} diff --git a/BTCPayServer/Authentication/OpenId/AuthorizationEventHandler.cs b/BTCPayServer/Authentication/OpenId/AuthorizationEventHandler.cs deleted file mode 100644 index f9268d842..000000000 --- a/BTCPayServer/Authentication/OpenId/AuthorizationEventHandler.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; -using BTCPayServer.Models; -using BTCPayServer.Security; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Options; -using OpenIddict.Abstractions; -using OpenIddict.Server; - -namespace BTCPayServer.Authentication.OpenId -{ - public class AuthorizationEventHandler : BaseOpenIdGrantHandler - { - private readonly UserManager _userManager; - - public override async Task HandleAsync( - OpenIddictServerEvents.HandleAuthorizationRequest notification) - { - if (!notification.Context.Request.IsAuthorizationRequest()) - { - return OpenIddictServerEventState.Unhandled; - } - - var auth = await notification.Context.HttpContext.AuthenticateAsync(); - if (!auth.Succeeded) - { - // If the client application request promptless authentication, - // return an error indicating that the user is not logged in. - if (notification.Context.Request.HasPrompt(OpenIdConnectConstants.Prompts.None)) - { - var properties = new AuthenticationProperties(new Dictionary - { - [OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired, - [OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in." - }); - - - // Ask OpenIddict to return a login_required error to the client application. - await notification.Context.HttpContext.ForbidAsync(properties); - notification.Context.HandleResponse(); - return OpenIddictServerEventState.Handled; - } - - await notification.Context.HttpContext.ChallengeAsync(); - notification.Context.HandleResponse(); - return OpenIddictServerEventState.Handled; - } - - // Retrieve the profile of the logged in user. - var user = await _userManager.GetUserAsync(auth.Principal); - if (user == null) - { - notification.Context.Reject( - error: OpenIddictConstants.Errors.InvalidGrant, - description: "An internal error has occurred"); - - return OpenIddictServerEventState.Handled; - } - - // Create a new authentication ticket. - var ticket = await CreateTicketAsync(notification.Context.Request, user); - - // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. - notification.Context.Validate(ticket); - return OpenIddictServerEventState.Handled; - } - - public AuthorizationEventHandler( - UserManager userManager, SignInManager signInManager, - IOptions identityOptions) : base(signInManager, identityOptions) - { - _userManager = userManager; - } - } -} diff --git a/BTCPayServer/Authentication/OpenId/BaseOpenIdGrantHandler.cs b/BTCPayServer/Authentication/OpenId/BaseOpenIdGrantHandler.cs index 9bed543f8..59c885ed9 100644 --- a/BTCPayServer/Authentication/OpenId/BaseOpenIdGrantHandler.cs +++ b/BTCPayServer/Authentication/OpenId/BaseOpenIdGrantHandler.cs @@ -1,13 +1,11 @@ -using System.Collections.Generic; -using System.Security.Claims; using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; -using OpenIddict.Abstractions; +using OpenIddict.Core; using OpenIddict.Server; namespace BTCPayServer.Authentication.OpenId @@ -15,88 +13,30 @@ namespace BTCPayServer.Authentication.OpenId public abstract class BaseOpenIdGrantHandler : IOpenIddictServerEventHandler where T : class, IOpenIddictServerEvent { + private readonly OpenIddictApplicationManager _applicationManager; + private readonly OpenIddictAuthorizationManager _authorizationManager; protected readonly SignInManager _signInManager; protected readonly IOptions _identityOptions; - protected BaseOpenIdGrantHandler(SignInManager signInManager, + protected BaseOpenIdGrantHandler( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, IOptions identityOptions) { + _applicationManager = applicationManager; + _authorizationManager = authorizationManager; _signInManager = signInManager; _identityOptions = identityOptions; } + protected async Task CreateTicketAsync( OpenIdConnectRequest request, ApplicationUser user, AuthenticationProperties properties = null) { - // Create a new ClaimsPrincipal containing the claims that - // will be used to create an id_token, a token or a code. - var principal = await _signInManager.CreateUserPrincipalAsync(user); - - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket(principal, properties, - OpenIddictServerDefaults.AuthenticationScheme); - - if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType()) - { - // Note: in this sample, the granted scopes match the requested scope - // but you may want to allow the user to uncheck specific scopes. - // For that, simply restrict the list of scopes before calling SetScopes. - ticket.SetScopes(request.GetScopes()); - } - - foreach (var claim in ticket.Principal.Claims) - { - claim.SetDestinations(GetDestinations(claim, ticket)); - } - - return ticket; - } - - private IEnumerable GetDestinations(Claim claim, AuthenticationTicket ticket) - { - // Note: by default, claims are NOT automatically included in the access and identity tokens. - // To allow OpenIddict to serialize them, you must attach them a destination, that specifies - // whether they should be included in access tokens, in identity tokens or in both. - - - switch (claim.Type) - { - case OpenIddictConstants.Claims.Name: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (ticket.HasScope(OpenIddictConstants.Scopes.Profile)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - - case OpenIddictConstants.Claims.Email: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (ticket.HasScope(OpenIddictConstants.Scopes.Email)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - - case OpenIddictConstants.Claims.Role: - yield return OpenIddictConstants.Destinations.AccessToken; - - if (ticket.HasScope(OpenIddictConstants.Scopes.Roles)) - yield return OpenIddictConstants.Destinations.IdentityToken; - - yield break; - default: - if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType) - { - // Never include the security stamp in the access and identity tokens, as it's a secret value. - yield break; - } - else - { - yield return OpenIddictConstants.Destinations.AccessToken; - yield break; - } - } + return await OpenIdExtensions.CreateAuthenticationTicket(_applicationManager, _authorizationManager, + _identityOptions.Value, _signInManager, request, user, properties); } public abstract Task HandleAsync(T notification); diff --git a/BTCPayServer/Authentication/OpenId/ClientCredentialsGrantTypeEventHandler.cs b/BTCPayServer/Authentication/OpenId/ClientCredentialsGrantTypeEventHandler.cs index 77f455b7c..4708380f6 100644 --- a/BTCPayServer/Authentication/OpenId/ClientCredentialsGrantTypeEventHandler.cs +++ b/BTCPayServer/Authentication/OpenId/ClientCredentialsGrantTypeEventHandler.cs @@ -21,9 +21,12 @@ namespace BTCPayServer.Authentication.OpenId private readonly UserManager _userManager; - public ClientCredentialsGrantTypeEventHandler(SignInManager signInManager, + public ClientCredentialsGrantTypeEventHandler( OpenIddictApplicationManager applicationManager, - IOptions identityOptions, UserManager userManager) : base(signInManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, + IOptions identityOptions, + UserManager userManager) : base(applicationManager, authorizationManager, signInManager, identityOptions) { _applicationManager = applicationManager; diff --git a/BTCPayServer/Authentication/OpenId/LogoutEventHandler.cs b/BTCPayServer/Authentication/OpenId/LogoutEventHandler.cs index 70c612d66..62c5107b8 100644 --- a/BTCPayServer/Authentication/OpenId/LogoutEventHandler.cs +++ b/BTCPayServer/Authentication/OpenId/LogoutEventHandler.cs @@ -1,21 +1,28 @@ using System; using System.Threading.Tasks; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using OpenIddict.Core; using OpenIddict.Server; namespace BTCPayServer.Authentication.OpenId { - - public class LogoutEventHandler: BaseOpenIdGrantHandler + public class LogoutEventHandler : BaseOpenIdGrantHandler { - public LogoutEventHandler(SignInManager signInManager, IOptions identityOptions) : base(signInManager, identityOptions) + public LogoutEventHandler( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, IOptions identityOptions) : base( + applicationManager, authorizationManager, + signInManager, identityOptions) { } - public override async Task HandleAsync(OpenIddictServerEvents.HandleLogoutRequest notification) + public override async Task HandleAsync( + OpenIddictServerEvents.HandleLogoutRequest notification) { // Ask ASP.NET Core Identity to delete the local and external cookies created // when the user agent is redirected from the external identity provider diff --git a/BTCPayServer/Authentication/OpenId/OpenIdExtensions.cs b/BTCPayServer/Authentication/OpenId/OpenIdExtensions.cs new file mode 100644 index 000000000..5b8b755b5 --- /dev/null +++ b/BTCPayServer/Authentication/OpenId/OpenIdExtensions.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; +using BTCPayServer.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using OpenIddict.Abstractions; +using OpenIddict.Core; +using OpenIddict.Server; + +namespace BTCPayServer.Authentication.OpenId +{ + public static class OpenIdExtensions + { + public static async Task CreateAuthenticationTicket( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + IdentityOptions identityOptions, + SignInManager signInManager, + OpenIdConnectRequest request, + ApplicationUser user, + AuthenticationProperties properties = null) + { + // Create a new ClaimsPrincipal containing the claims that + // will be used to create an id_token, a token or a code. + var principal = await signInManager.CreateUserPrincipalAsync(user); + + // Create a new authentication ticket holding the user identity. + var ticket = new AuthenticationTicket(principal, properties, + OpenIddictServerDefaults.AuthenticationScheme); + + if (!request.IsAuthorizationCodeGrantType() && !request.IsRefreshTokenGrantType()) + { + ticket.SetScopes(request.GetScopes()); + } + else if (request.IsAuthorizationCodeGrantType() && + string.IsNullOrEmpty(ticket.GetInternalAuthorizationId())) + { + var app = await applicationManager.FindByClientIdAsync(request.ClientId); + var authorizationId = await IsUserAuthorized(authorizationManager, request, user.Id, app.Id); + if (!string.IsNullOrEmpty(authorizationId)) + { + ticket.SetInternalAuthorizationId(authorizationId); + } + } + + foreach (var claim in ticket.Principal.Claims) + { + claim.SetDestinations(GetDestinations(identityOptions, claim, ticket)); + } + + return ticket; + } + + private static IEnumerable GetDestinations(IdentityOptions identityOptions, Claim claim, + AuthenticationTicket ticket) + { + // Note: by default, claims are NOT automatically included in the access and identity tokens. + // To allow OpenIddict to serialize them, you must attach them a destination, that specifies + // whether they should be included in access tokens, in identity tokens or in both. + + + switch (claim.Type) + { + case OpenIddictConstants.Claims.Name: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (ticket.HasScope(OpenIddictConstants.Scopes.Profile)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Email: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (ticket.HasScope(OpenIddictConstants.Scopes.Email)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + + case OpenIddictConstants.Claims.Role: + yield return OpenIddictConstants.Destinations.AccessToken; + + if (ticket.HasScope(OpenIddictConstants.Scopes.Roles)) + yield return OpenIddictConstants.Destinations.IdentityToken; + + yield break; + default: + if (claim.Type == identityOptions.ClaimsIdentity.SecurityStampClaimType) + { + // Never include the security stamp in the access and identity tokens, as it's a secret value. + yield break; + } + else + { + yield return OpenIddictConstants.Destinations.AccessToken; + yield break; + } + } + } + + public static async Task IsUserAuthorized( + OpenIddictAuthorizationManager authorizationManager, + OpenIdConnectRequest request, string userId, string applicationId) + { + var authorizations = + await authorizationManager.ListAsync(queryable => + queryable.Where(authorization => + authorization.Subject.Equals(userId, StringComparison.OrdinalIgnoreCase) && + applicationId.Equals(authorization.Application.Id, StringComparison.OrdinalIgnoreCase) && + authorization.Status.Equals(OpenIddictConstants.Statuses.Valid, + StringComparison.OrdinalIgnoreCase))); + + + 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) => (tuple.Id, Scopes: tuple.Item1.Result)) + .Where((tuple) => !request.GetScopes().Except(tuple.Scopes).Any()); + + if (authorizationsWithSufficientScopes.Any()) + { + return authorizationsWithSufficientScopes.First().Id; + } + } + + return null; + } + } +} diff --git a/BTCPayServer/Authentication/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs b/BTCPayServer/Authentication/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs index 678a77574..413a45229 100644 --- a/BTCPayServer/Authentication/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs +++ b/BTCPayServer/Authentication/OpenId/OpenIdGrantHandlerCheckCanSignIn.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using OpenIddict.Core; using OpenIddict.Server; namespace BTCPayServer.Authentication.OpenId @@ -14,8 +16,12 @@ namespace BTCPayServer.Authentication.OpenId { private readonly UserManager _userManager; - protected OpenIdGrantHandlerCheckCanSignIn(SignInManager signInManager, - IOptions identityOptions, UserManager userManager) : base(signInManager, + protected OpenIdGrantHandlerCheckCanSignIn( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, + IOptions identityOptions, UserManager userManager) : base( + applicationManager, authorizationManager, signInManager, identityOptions) { _userManager = userManager; @@ -36,7 +42,6 @@ namespace BTCPayServer.Authentication.OpenId var scheme = notification.Context.Scheme.Name; var authenticateResult = (await notification.Context.HttpContext.AuthenticateAsync(scheme)); - var user = await _userManager.GetUserAsync(authenticateResult.Principal); if (user == null) { diff --git a/BTCPayServer/Authentication/OpenId/PasswordGrantTypeEventHandler.cs b/BTCPayServer/Authentication/OpenId/PasswordGrantTypeEventHandler.cs index 2648051cc..742782c95 100644 --- a/BTCPayServer/Authentication/OpenId/PasswordGrantTypeEventHandler.cs +++ b/BTCPayServer/Authentication/OpenId/PasswordGrantTypeEventHandler.cs @@ -1,10 +1,12 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using BTCPayServer.Services.U2F; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using OpenIddict.Core; using OpenIddict.Server; namespace BTCPayServer.Authentication.OpenId @@ -14,9 +16,13 @@ namespace BTCPayServer.Authentication.OpenId private readonly UserManager _userManager; private readonly U2FService _u2FService; - public PasswordGrantTypeEventHandler(SignInManager signInManager, + public PasswordGrantTypeEventHandler( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, UserManager userManager, - IOptions identityOptions, U2FService u2FService) : base(signInManager, identityOptions) + IOptions identityOptions, U2FService u2FService) : base(applicationManager, + authorizationManager, signInManager, identityOptions) { _userManager = userManager; _u2FService = u2FService; @@ -54,4 +60,4 @@ namespace BTCPayServer.Authentication.OpenId return OpenIddictServerEventState.Handled; } } -} \ No newline at end of file +} diff --git a/BTCPayServer/Authentication/OpenId/RefreshTokenGrantTypeEventHandler.cs b/BTCPayServer/Authentication/OpenId/RefreshTokenGrantTypeEventHandler.cs index 8fb42f748..30e8b45e0 100644 --- a/BTCPayServer/Authentication/OpenId/RefreshTokenGrantTypeEventHandler.cs +++ b/BTCPayServer/Authentication/OpenId/RefreshTokenGrantTypeEventHandler.cs @@ -1,14 +1,20 @@ using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId.Models; using BTCPayServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; +using OpenIddict.Core; namespace BTCPayServer.Authentication.OpenId { public class RefreshTokenGrantTypeEventHandler : OpenIdGrantHandlerCheckCanSignIn { - public RefreshTokenGrantTypeEventHandler(SignInManager signInManager, - IOptions identityOptions, UserManager userManager) : base(signInManager, + public RefreshTokenGrantTypeEventHandler( + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + SignInManager signInManager, + IOptions identityOptions, UserManager userManager) : base( + applicationManager, authorizationManager, signInManager, identityOptions, userManager) { } @@ -18,4 +24,4 @@ namespace BTCPayServer.Authentication.OpenId return request.IsRefreshTokenGrantType(); } } -} \ No newline at end of file +} diff --git a/BTCPayServer/Controllers/AuthorizationController.cs b/BTCPayServer/Controllers/AuthorizationController.cs new file mode 100644 index 000000000..6f5747555 --- /dev/null +++ b/BTCPayServer/Controllers/AuthorizationController.cs @@ -0,0 +1,136 @@ +/* + * 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 AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Primitives; +using BTCPayServer.Authentication.OpenId; +using BTCPayServer.Authentication.OpenId.Models; +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 OpenIddict.Abstractions; +using OpenIddict.Core; +using OpenIddict.Server; + +namespace BTCPayServer.Controllers +{ + public class AuthorizationController : Controller + { + private readonly OpenIddictApplicationManager _applicationManager; + private readonly SignInManager _signInManager; + private readonly OpenIddictAuthorizationManager _authorizationManager; + private readonly UserManager _userManager; + private readonly IOptions _IdentityOptions; + + public AuthorizationController( + OpenIddictApplicationManager applicationManager, + SignInManager signInManager, + OpenIddictAuthorizationManager authorizationManager, + UserManager userManager, + IOptions identityOptions) + { + _applicationManager = applicationManager; + _signInManager = signInManager; + _authorizationManager = authorizationManager; + _userManager = userManager; + _IdentityOptions = identityOptions; + } + + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] + [HttpGet("/connect/authorize")] + public async Task Authorize(OpenIdConnectRequest request) + { + // 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(request, "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.Scope + }); + } + + [Authorize(AuthenticationSchemes = Policies.CookieAuthentication)] + [HttpPost("/connect/authorize")] + public async Task Authorize(OpenIdConnectRequest request, + string consent, bool createAuthorization = true) + { + 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(OpenIddictServerDefaults.AuthenticationScheme); + } + + + // Create a new authentication ticket. + var ticket = + await OpenIdExtensions.CreateAuthenticationTicket(_applicationManager, _authorizationManager, + _IdentityOptions.Value, _signInManager, + request, user); + if (createAuthorization) + { + var application = await _applicationManager.FindByClientIdAsync(request.ClientId); + var authorization = await _authorizationManager.CreateAsync(User, user.Id, application.Id, + type, ticket.GetScopes().ToImmutableArray(), + ticket.Properties.Items.ToImmutableDictionary()); + ticket.SetInternalAuthorizationId(authorization.Id); + } + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + } + } +} diff --git a/BTCPayServer/Extensions/OpenIddictExtensions.cs b/BTCPayServer/Extensions/OpenIddictExtensions.cs index df33edeb9..e16f81525 100644 --- a/BTCPayServer/Extensions/OpenIddictExtensions.cs +++ b/BTCPayServer/Extensions/OpenIddictExtensions.cs @@ -10,30 +10,21 @@ namespace BTCPayServer { public static class OpenIddictExtensions { - private static SecurityKey _key = null; public static SecurityKey GetSigningKey(IConfiguration configuration) { - if (_key != null) - { - return _key; - } + var file = Path.Combine(configuration.GetDataDir(), "rsaparams"); - - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(2048); - + var rsa = new RSACryptoServiceProvider(2048); if (File.Exists(file)) { - RSA.FromXmlString2(File.ReadAllText(file)); + rsa.FromXmlString2(File.ReadAllText(file)); } else { - var contents = RSA.ToXmlString2(true); + var contents = rsa.ToXmlString2(true); File.WriteAllText(file, contents); } - - RSAParameters KeyParam = RSA.ExportParameters(true); - _key = new RsaSecurityKey(KeyParam); - return _key; + return new RsaSecurityKey(rsa.ExportParameters(true));; } public static OpenIddictServerBuilder ConfigureSigningKey(this OpenIddictServerBuilder builder, IConfiguration configuration) diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs index 22566ac59..728871753 100644 --- a/BTCPayServer/Hosting/Startup.cs +++ b/BTCPayServer/Hosting/Startup.cs @@ -147,7 +147,7 @@ namespace BTCPayServer.Hosting }) .AddServer(options => { - + options.EnableRequestCaching(); //Disabled so that Tor works with OpenIddict too options.DisableHttpsRequirement(); // Register the ASP.NET Core MVC binder used by OpenIddict. @@ -182,7 +182,6 @@ namespace BTCPayServer.Hosting options.AddEventHandler(); options.AddEventHandler(); options.AddEventHandler(); - options.AddEventHandler(); options.AddEventHandler(); options.ConfigureSigningKey(Configuration); diff --git a/BTCPayServer/Models/Authorization/AuthorizeViewModel.cs b/BTCPayServer/Models/Authorization/AuthorizeViewModel.cs new file mode 100644 index 000000000..05dcf577f --- /dev/null +++ b/BTCPayServer/Models/Authorization/AuthorizeViewModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace BTCPayServer.Models.Authorization +{ + public class AuthorizeViewModel + { + [Display(Name = "Application")] public string ApplicationName { get; set; } + + [BindNever] public string RequestId { get; set; } + + [Display(Name = "Scope")] public string Scope { get; set; } + } +} diff --git a/BTCPayServer/Views/Authorization/Authorize.cshtml b/BTCPayServer/Views/Authorization/Authorize.cshtml new file mode 100644 index 000000000..92a0a8773 --- /dev/null +++ b/BTCPayServer/Views/Authorization/Authorize.cshtml @@ -0,0 +1,31 @@ +@model BTCPayServer.Models.Authorization.AuthorizeViewModel +
+ +
+
+
+
+

Authorization Request

+
+

@Model.ApplicationName is requesting access to your account.

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