diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 558293e1d..93ec8eb6c 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "7.0.10",
+ "version": "8.0.5",
"commands": [
"dotnet-ef"
]
diff --git a/.idea/.idea.LexBox/.idea/.gitignore b/.idea/.idea.LexBox/.idea/.gitignore
index 4ec73edd9..daa11e94f 100644
--- a/.idea/.idea.LexBox/.idea/.gitignore
+++ b/.idea/.idea.LexBox/.idea/.gitignore
@@ -11,3 +11,5 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+# GitHub Copilot persisted chat sessions
+/copilot/chatSessions
diff --git a/.idea/.idea.LexBox/.idea/dataSources.xml b/.idea/.idea.LexBox/.idea/dataSources.xml
index eceae86ec..b6ad8028d 100644
--- a/.idea/.idea.LexBox/.idea/dataSources.xml
+++ b/.idea/.idea.LexBox/.idea/dataSources.xml
@@ -5,32 +5,17 @@
postgresql
true
org.postgresql.Driver
- jdbc:postgresql://localhost/lexbox
+ jdbc:postgresql://localhost:5433/lexbox
-
+
-
$ProjectFileDir$
-
- mariadb
- true
- redmine data
- org.mariadb.jdbc.Driver
- jdbc:mariadb://localhost:3306
-
-
-
-
-
-
- $ProjectFileDir$
-
postgresql
true
@@ -65,5 +50,41 @@
$ProjectFileDir$
+
+ mongo
+ true
+ true
+ com.dbschema.MongoJdbcDriver
+ mongodb://localhost:27018
+ $ProjectFileDir$
+
+
+ mongo
+ true
+ true
+ com.dbschema.MongoJdbcDriver
+ mongodb://localhost:27017
+
+
+
+
+
+ $ProjectFileDir$
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:C:\dev\LexBox\backend\LocalWebApp\sena-3.sqlite
+
+
+
+ $ProjectFileDir$
+
+
+ file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar
+
+
+
-
+
\ No newline at end of file
diff --git a/.idea/.idea.LexBox/.idea/indexLayout.xml b/.idea/.idea.LexBox/.idea/indexLayout.xml
index 3a934820b..24b63bcb7 100644
--- a/.idea/.idea.LexBox/.idea/indexLayout.xml
+++ b/.idea/.idea.LexBox/.idea/indexLayout.xml
@@ -20,4 +20,4 @@
frontend/.svelte-kit/output
-
\ No newline at end of file
+
diff --git a/.idea/.idea.LexBox/.idea/vcs.xml b/.idea/.idea.LexBox/.idea/vcs.xml
index 30edf8684..9041d4ebe 100644
--- a/.idea/.idea.LexBox/.idea/vcs.xml
+++ b/.idea/.idea.LexBox/.idea/vcs.xml
@@ -12,5 +12,6 @@
+
-
\ No newline at end of file
+
diff --git a/backend/LexBoxApi/Auth/AuthKernel.cs b/backend/LexBoxApi/Auth/AuthKernel.cs
index 892deaa5c..38fae368b 100644
--- a/backend/LexBoxApi/Auth/AuthKernel.cs
+++ b/backend/LexBoxApi/Auth/AuthKernel.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
@@ -5,12 +6,14 @@
using LexBoxApi.Auth.Requirements;
using LexBoxApi.Controllers;
using LexCore.Auth;
+using LexData;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Logging;
using Microsoft.OpenApi.Models;
+using OpenIddict.Validation.AspNetCore;
namespace LexBoxApi.Auth;
@@ -66,6 +69,8 @@ public static void AddLexBoxAuth(IServiceCollection services,
.BindConfiguration("Authentication:Jwt")
.ValidateDataAnnotations()
.ValidateOnStart();
+ services.AddOptions()
+ .BindConfiguration("Authentication:OpenId");
services.AddAuthentication(options =>
{
options.DefaultScheme = DefaultScheme;
@@ -78,7 +83,13 @@ public static void AddLexBoxAuth(IServiceCollection services,
{
options.ForwardDefaultSelector = context =>
{
-
+ if (context.Request.Headers.ContainsKey("Authorization") &&
+ context.Request.Headers.Authorization.ToString().StartsWith("Bearer") &&
+ context.RequestServices.GetService>()?.Value.Enable == true)
+ {
+ //fow now this will use oauth
+ return OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
+ }
if (context.Request.IsJwtRequest())
{
return JwtBearerDefaults.AuthenticationScheme;
@@ -101,9 +112,9 @@ public static void AddLexBoxAuth(IServiceCollection services,
.AddCookie(options =>
{
configuration.Bind("Authentication:Cookie", options);
- options.LoginPath = "/api/login";
+ options.LoginPath = "/login";
options.Cookie.Name = AuthCookieName;
- options.ForwardChallenge = JwtBearerDefaults.AuthenticationScheme;
+ // options.ForwardChallenge = JwtBearerDefaults.AuthenticationScheme;
options.ForwardForbid = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
@@ -152,7 +163,8 @@ public static void AddLexBoxAuth(IServiceCollection services,
context.HandleResponse();
var loginController = context.HttpContext.RequestServices.GetRequiredService();
loginController.ControllerContext.HttpContext = context.HttpContext;
- var redirectTo = await loginController.CompleteGoogleLogin(context.Principal, context.Properties?.RedirectUri);
+ //using context.ReturnUri and not context.Properties.RedirectUri because the latter is null
+ var redirectTo = await loginController.CompleteGoogleLogin(context.Principal, context.ReturnUri);
context.HttpContext.Response.Redirect(redirectTo);
};
});
@@ -185,6 +197,65 @@ public static void AddLexBoxAuth(IServiceCollection services,
}
});
});
+
+ var openIdOptions = configuration.GetSection("Authentication:OpenId").Get();
+ if (openIdOptions?.Enable == true) AddOpenId(services, environment);
+ }
+
+ private static void AddOpenId(IServiceCollection services, IWebHostEnvironment environment)
+ {
+ services.Add(ScopeRequestFixer.Descriptor.ServiceDescriptor);
+ //openid server
+ services.AddOpenIddict()
+ .AddCore(options =>
+ {
+ options.UseEntityFrameworkCore().UseDbContext();
+ options.UseQuartz();
+ })
+ .AddServer(options =>
+ {
+ options.RegisterScopes("openid", "profile", "email");
+ //todo add application claims
+ options.RegisterClaims("aud", "email", "exp", "iss", "iat", "sub", "name");
+ options.SetAuthorizationEndpointUris("api/oauth/open-id-auth");
+ options.SetTokenEndpointUris("api/oauth/token");
+ options.SetIntrospectionEndpointUris("api/oauth/introspect");
+ options.SetUserinfoEndpointUris("api/oauth/userinfo");
+ options.Configure(serverOptions => serverOptions.Handlers.Add(ScopeRequestFixer.Descriptor));
+
+ options.AllowAuthorizationCodeFlow()
+ .AllowRefreshTokenFlow();
+
+ options.RequireProofKeyForCodeExchange();//best practice to use PKCE with auth code flow and no implicit flow
+
+ options.IgnoreResponseTypePermissions();
+ options.IgnoreScopePermissions();
+ if (environment.IsDevelopment())
+ {
+ options.AddDevelopmentEncryptionCertificate();
+ options.AddDevelopmentSigningCertificate();
+ }
+ else
+ {
+ //see docs: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
+ throw new NotImplementedException("need to implement loading keys from a file");
+ }
+
+ var aspnetCoreBuilder = options.UseAspNetCore()
+ .EnableAuthorizationEndpointPassthrough()
+ .EnableTokenEndpointPassthrough();
+ if (environment.IsDevelopment())
+ {
+ aspnetCoreBuilder.DisableTransportSecurityRequirement();
+ }
+ })
+ .AddValidation(options =>
+ {
+ options.UseLocalServer();
+ options.UseAspNetCore();
+ options.AddAudiences(Enum.GetValues().Where(a => a != LexboxAudience.Unknown).Select(a => a.ToString()).ToArray());
+ options.EnableAuthorizationEntryValidation();
+ });
}
public static AuthorizationPolicyBuilder RequireDefaultLexboxAuth(this AuthorizationPolicyBuilder builder)
diff --git a/backend/LexBoxApi/Auth/OpenIdOptions.cs b/backend/LexBoxApi/Auth/OpenIdOptions.cs
new file mode 100644
index 000000000..eeef9e5e2
--- /dev/null
+++ b/backend/LexBoxApi/Auth/OpenIdOptions.cs
@@ -0,0 +1,9 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace LexBoxApi.Auth;
+
+public class OpenIdOptions
+{
+ [Required]
+ public required bool Enable { get; set; }
+}
diff --git a/backend/LexBoxApi/Auth/ScopeRequestFixer.cs b/backend/LexBoxApi/Auth/ScopeRequestFixer.cs
new file mode 100644
index 000000000..9048091ca
--- /dev/null
+++ b/backend/LexBoxApi/Auth/ScopeRequestFixer.cs
@@ -0,0 +1,28 @@
+using OpenIddict.Abstractions;
+using OpenIddict.Server;
+
+namespace LexBoxApi.Auth;
+
+///
+/// the MSAL library makes requests with the scope parameter, which is invalid, this attempts to remove the scope before it's rejected
+///
+public sealed class ScopeRequestFixer : IOpenIddictServerHandler
+{
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(OpenIddictServerHandlers.Exchange.ValidateResourceOwnerCredentialsParameters.Descriptor.Order + 1)
+ .SetType(OpenIddictServerHandlerType.Custom)
+ .Build();
+
+ public ValueTask HandleAsync(OpenIddictServerEvents.ValidateTokenRequestContext context)
+ {
+ if (!string.IsNullOrEmpty(context.Request.Scope) && (context.Request.IsAuthorizationCodeGrantType() ||
+ context.Request.IsDeviceCodeGrantType()))
+ {
+ context.Request.Scope = null;
+ }
+
+ return default;
+ }
+}
diff --git a/backend/LexBoxApi/Controllers/LoginController.cs b/backend/LexBoxApi/Controllers/LoginController.cs
index 944ead716..34466ab10 100644
--- a/backend/LexBoxApi/Controllers/LoginController.cs
+++ b/backend/LexBoxApi/Controllers/LoginController.cs
@@ -8,13 +8,9 @@
using LexCore.Auth;
using LexData;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.AspNetCore.Http;
-using LexCore.Entities;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Google;
@@ -28,8 +24,7 @@ public class LoginController(
LoggedInContext loggedInContext,
EmailService emailService,
UserService userService,
- TurnstileService turnstileService,
- ProjectService projectService)
+ TurnstileService turnstileService)
: ControllerBase
{
///
@@ -38,7 +33,6 @@ public class LoginController(
///
[HttpGet("loginRedirect")]
[AllowAnyAudience]
-
public async Task LoginRedirect(
string jwt, // This is required because auth looks for a jwt in the query string
string returnTo)
@@ -53,6 +47,7 @@ public async Task LoginRedirect(
return await EmailLinkExpired();
}
}
+
await HttpContext.SignInAsync(User,
new AuthenticationProperties { IsPersistent = true });
return Redirect(returnTo);
@@ -87,6 +82,7 @@ public async Task CompleteGoogleLogin(ClaimsPrincipal? principal, string
{
(authUser, userEntity) = await lexAuthService.GetUser(googleEmail);
}
+
if (authUser is null)
{
authUser = new LexAuthUser()
@@ -102,19 +98,20 @@ public async Task CompleteGoogleLogin(ClaimsPrincipal? principal, string
Locale = locale ?? LexCore.Entities.User.DefaultLocalizationCode,
Locked = null,
};
- var queryParams = new Dictionary() {
- {"email", googleEmail},
- {"name", googleName},
- {"returnTo", returnTo},
+ var queryParams = new Dictionary()
+ {
+ { "email", googleEmail }, { "name", googleName }, { "returnTo", returnTo },
};
var queryString = QueryString.Create(queryParams);
returnTo = "/register" + queryString.ToString();
}
+
if (userEntity is not null && !foundGoogleId)
{
userEntity.GoogleId = googleId;
await lexBoxDbContext.SaveChangesAsync();
}
+
await HttpContext.SignInAsync(authUser.GetPrincipal("google"),
new AuthenticationProperties { IsPersistent = true });
return returnTo;
@@ -158,6 +155,7 @@ public async Task> VerifyEmail(
return Redirect(returnTo);
}
+
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -219,7 +217,9 @@ public async Task ForgotPassword(ForgotPasswordInput input)
return Ok();
}
- public record ResetPasswordRequest([Required(AllowEmptyStrings = false)] string PasswordHash, int? PasswordStrength);
+ public record ResetPasswordRequest(
+ [Required(AllowEmptyStrings = false)] string PasswordHash,
+ int? PasswordStrength);
[HttpPost("resetPassword")]
[RequireAudience(LexboxAudience.ForgotPassword)]
diff --git a/backend/LexBoxApi/Controllers/OauthController.cs b/backend/LexBoxApi/Controllers/OauthController.cs
new file mode 100644
index 000000000..b7024a295
--- /dev/null
+++ b/backend/LexBoxApi/Controllers/OauthController.cs
@@ -0,0 +1,307 @@
+using System.Security.Claims;
+using System.Text.Json;
+using LexBoxApi.Auth;
+using LexCore.Auth;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+using OpenIddict.Server.AspNetCore;
+
+namespace LexBoxApi.Controllers;
+
+[ApiController]
+[Route("/api/oauth")]
+public class OauthController(
+ LoggedInContext loggedInContext,
+ IOpenIddictApplicationManager applicationManager,
+ IOpenIddictAuthorizationManager authorizationManager
+) : ControllerBase
+{
+
+ [HttpGet("open-id-auth")]
+ [HttpPost("open-id-auth")]
+ [ProducesResponseType(400)]
+ [ProducesDefaultResponseType]
+ public async Task Authorize()
+ {
+ var request = HttpContext.GetOpenIddictServerRequest();
+ if (request is null)
+ {
+ return BadRequest();
+ }
+
+ if (IsAcceptRequest())
+ {
+ return await FinishSignIn(loggedInContext.User, request);
+ }
+
+ // Retrieve the user principal stored in the authentication cookie.
+ // If the user principal can't be extracted or the cookie is too old, redirect the user to the login page.
+ var result = await HttpContext.AuthenticateAsync();
+ var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null;
+ if (!result.Succeeded ||
+ lexAuthUser is null ||
+ request.HasPrompt(OpenIddictConstants.Prompts.Login) ||
+ IsExpired(request, result))
+ {
+ // If the client application requested promptless authentication,
+ // return an error indicating that the user is not logged in.
+ if (request.HasPrompt(OpenIddictConstants.Prompts.None))
+ {
+ return Forbid(
+ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] =
+ OpenIddictConstants.Errors.LoginRequired,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
+ "The user is not logged in."
+ }));
+ }
+
+ // To avoid endless login -> authorization redirects, the prompt=login flag
+ // is removed from the authorization request payload before redirecting the user.
+ var prompt = string.Join(" ", request.GetPrompts().Remove(OpenIddictConstants.Prompts.Login));
+
+ var parameters = Request.HasFormContentType
+ ? Request.Form.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList()
+ : Request.Query.Where(parameter => parameter.Key != OpenIddictConstants.Parameters.Prompt).ToList();
+
+ parameters.Add(KeyValuePair.Create(OpenIddictConstants.Parameters.Prompt, new StringValues(prompt)));
+
+ return Challenge(
+ authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties
+ {
+ RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters)
+ });
+ }
+
+ var userId = lexAuthUser.Id.ToString();
+ var requestClientId = request.ClientId;
+ ArgumentException.ThrowIfNullOrEmpty(requestClientId);
+ var application = await applicationManager.FindByClientIdAsync(requestClientId) ??
+ throw new InvalidOperationException(
+ "Details concerning the calling client application cannot be found.");
+ var applicationId = await applicationManager.GetIdAsync(application) ??
+ throw new InvalidOperationException("The calling client application could not be found.");
+
+ // Retrieve the permanent authorizations associated with the user and the calling client application.
+ var authorizations = await authorizationManager.FindAsync(
+ subject: userId,
+ client: applicationId,
+ status: OpenIddictConstants.Statuses.Valid,
+ type: OpenIddictConstants.AuthorizationTypes.Permanent,
+ scopes: request.GetScopes()).ToListAsync();
+
+ switch (await applicationManager.GetConsentTypeAsync(application))
+ {
+ // If the consent is implicit or if an authorization was found,
+ // return an authorization response without displaying the consent form.
+ case OpenIddictConstants.ConsentTypes.Implicit:
+ case OpenIddictConstants.ConsentTypes.External when authorizations.Count is not 0:
+ case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(OpenIddictConstants.Prompts.Consent):
+
+ return await FinishSignIn(lexAuthUser, request, applicationId, authorizations);
+
+ // If the consent is external (e.g when authorizations are granted by a sysadmin),
+ // immediately return an error if no authorization can be found in the database.
+ case OpenIddictConstants.ConsentTypes.External when authorizations.Count is 0:
+ return Forbid(
+ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
+ "The logged in user is not allowed to access this client application."
+ }));
+
+ // At this point, no authorization was found in the database and an error must be returned
+ // if the client application specified prompt=none in the authorization request.
+ case OpenIddictConstants.ConsentTypes.Explicit when request.HasPrompt(OpenIddictConstants.Prompts.None):
+ case OpenIddictConstants.ConsentTypes.Systematic when request.HasPrompt(OpenIddictConstants.Prompts.None):
+ return Forbid(
+ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
+ "Interactive user consent is required."
+ }));
+
+ // In every other case, send user to consent page
+ default:
+ var parameters = Request.HasFormContentType
+ ? Request.Form.ToList()
+ : Request.Query.ToList();
+ var data = JsonSerializer.Serialize(parameters.ToDictionary(pair => pair.Key, pair => pair.Value.ToString()));
+ var queryString = new QueryString()
+ .Add("appName", await applicationManager.GetDisplayNameAsync(application) ?? "Unknown app")
+ .Add("scope", request.Scope ?? "")
+ .Add("postback", data);
+ return Redirect($"/authorize{queryString.Value}");
+ }
+ }
+
+ private static bool IsExpired(OpenIddictRequest request, AuthenticateResult result)
+ {
+ // If a max_age parameter was provided, ensure that the cookie is not too old.
+ return (request.MaxAge != null && result.Properties?.IssuedUtc != null &&
+ DateTimeOffset.UtcNow - result.Properties.IssuedUtc >
+ TimeSpan.FromSeconds(request.MaxAge.Value));
+ }
+
+ private bool IsAcceptRequest()
+ {
+ return Request.Method == "POST" && Request.Form.ContainsKey("submit.accept") && User.Identity?.IsAuthenticated == true;
+ }
+
+ [HttpPost("token")]
+ [AllowAnonymous]
+ public async Task Exchange()
+ {
+ var request = HttpContext.GetOpenIddictServerRequest() ??
+ throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
+ // Retrieve the claims principal stored in the authorization code/refresh token.
+ var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ var lexAuthUser = result.Succeeded ? LexAuthUser.FromClaimsPrincipal(result.Principal) : null;
+ if (!result.Succeeded || lexAuthUser is null)
+ {
+ return Forbid(
+ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
+ "The token is no longer valid."
+ }));
+ }
+
+ return await FinishSignIn(lexAuthUser, request);
+ }
+
+ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request)
+ {
+ var requestClientId = request.ClientId;
+ ArgumentException.ThrowIfNullOrEmpty(requestClientId);
+ var application = await applicationManager.FindByClientIdAsync(requestClientId) ??
+ throw new InvalidOperationException(
+ "Details concerning the calling client application cannot be found.");
+ // Retrieve the permanent authorizations associated with the user and the calling client application.
+ var applicationId = await applicationManager.GetIdAsync(application) ?? throw new InvalidOperationException("The calling client application could not be found.");
+ var authorizations = await authorizationManager.FindAsync(
+ subject: lexAuthUser.Id.ToString(),
+ client: applicationId,
+ status: OpenIddictConstants.Statuses.Valid,
+ type: OpenIddictConstants.AuthorizationTypes.Permanent,
+ scopes: request.GetScopes()).ToListAsync();
+
+ //allow cors response for redirect hosts
+ var redirectUrisAsync = await applicationManager.GetRedirectUrisAsync(application);
+ Response.Headers.AccessControlAllowOrigin = redirectUrisAsync
+ .Select(uri => new Uri(uri).GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)).ToArray();
+
+ // Note: this check is here to ensure a malicious user can't abuse this POST-only endpoint and
+ // force it to return a valid response without the external authorization.
+ if (authorizations.Count is 0 &&
+ await applicationManager.HasConsentTypeAsync(application, OpenIddictConstants.ConsentTypes.External))
+ {
+ return Forbid(
+ authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ properties: new AuthenticationProperties(new Dictionary
+ {
+ [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.ConsentRequired,
+ [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] =
+ "The logged in user is not allowed to access this client application."
+ }));
+ }
+
+ return await FinishSignIn(lexAuthUser, request, applicationId, authorizations);
+ }
+ private async Task FinishSignIn(LexAuthUser lexAuthUser, OpenIddictRequest request, string applicationId, List