Skip to content

Commit

Permalink
configure openid connect (#809)
Browse files Browse the repository at this point in the history
* attempt to add openid connect support

* update dotnet ef tool version

* disable openId in production using a config option. This lets us figure out how to we want to do key storage in production

* fix redirect loop trying to send the user to the login page, if that's handled by aspnet it'll result in a redirect loop

* trim down claims check list since most aren't used, allow name claim to end up in ID token for testing purposes

* correct vite proxy to send traces to the collector and not dotnet.

* configure the login page to handle return urls

* allow application manager to be null in seeding data to allow for case where openId is disabled

* require pkce and disable implicit flow, enable oauth to work over http and with proper CORS headers

* create approval flow for oauth login

* correct vite proxy so https isn't used incorrectly when the backend redirects somewhere

* pass redirect uri along via google login, fix bug where redirect url was always null in `CompleteGoogleLogin` due to using the wrong property.

* Redesign authorize page

* extract oauth code out of LoginController.cs and into OauthController.cs, revert some changes made to vite.config.ts

---------

Co-authored-by: Tim Haasdyk <[email protected]>
  • Loading branch information
hahn-kev and myieye authored May 28, 2024
1 parent 1ce357a commit e3d7d16
Show file tree
Hide file tree
Showing 30 changed files with 2,392 additions and 102 deletions.
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.10",
"version": "8.0.5",
"commands": [
"dotnet-ef"
]
Expand Down
2 changes: 2 additions & 0 deletions .idea/.idea.LexBox/.idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 39 additions & 18 deletions .idea/.idea.LexBox/.idea/dataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/.idea.LexBox/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/.idea.LexBox/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 75 additions & 4 deletions backend/LexBoxApi/Auth/AuthKernel.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Text;
using LexBoxApi.Auth.Attributes;
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;

Expand Down Expand Up @@ -66,6 +69,8 @@ public static void AddLexBoxAuth(IServiceCollection services,
.BindConfiguration("Authentication:Jwt")
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddOptions<OpenIdOptions>()
.BindConfiguration("Authentication:OpenId");
services.AddAuthentication(options =>
{
options.DefaultScheme = DefaultScheme;
Expand All @@ -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<IOptions<OpenIdOptions>>()?.Value.Enable == true)
{
//fow now this will use oauth
return OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
}
if (context.Request.IsJwtRequest())
{
return JwtBearerDefaults.AuthenticationScheme;
Expand All @@ -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 =>
Expand Down Expand Up @@ -152,7 +163,8 @@ public static void AddLexBoxAuth(IServiceCollection services,
context.HandleResponse();
var loginController = context.HttpContext.RequestServices.GetRequiredService<LoginController>();
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);
};
});
Expand Down Expand Up @@ -185,6 +197,65 @@ public static void AddLexBoxAuth(IServiceCollection services,
}
});
});

var openIdOptions = configuration.GetSection("Authentication:OpenId").Get<OpenIdOptions>();
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<LexBoxDbContext>();
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<LexboxAudience>().Where(a => a != LexboxAudience.Unknown).Select(a => a.ToString()).ToArray());
options.EnableAuthorizationEntryValidation();
});
}

public static AuthorizationPolicyBuilder RequireDefaultLexboxAuth(this AuthorizationPolicyBuilder builder)
Expand Down
9 changes: 9 additions & 0 deletions backend/LexBoxApi/Auth/OpenIdOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;

namespace LexBoxApi.Auth;

public class OpenIdOptions
{
[Required]
public required bool Enable { get; set; }
}
28 changes: 28 additions & 0 deletions backend/LexBoxApi/Auth/ScopeRequestFixer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using OpenIddict.Abstractions;
using OpenIddict.Server;

namespace LexBoxApi.Auth;

/// <summary>
/// the MSAL library makes requests with the scope parameter, which is invalid, this attempts to remove the scope before it's rejected
/// </summary>
public sealed class ScopeRequestFixer : IOpenIddictServerHandler<OpenIddictServerEvents.ValidateTokenRequestContext>
{
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<OpenIddictServerEvents.ValidateTokenRequestContext>()
.UseSingletonHandler<ScopeRequestFixer>()
.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;
}
}
24 changes: 12 additions & 12 deletions backend/LexBoxApi/Controllers/LoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,8 +24,7 @@ public class LoginController(
LoggedInContext loggedInContext,
EmailService emailService,
UserService userService,
TurnstileService turnstileService,
ProjectService projectService)
TurnstileService turnstileService)
: ControllerBase
{
/// <summary>
Expand All @@ -38,7 +33,6 @@ public class LoginController(
/// </summary>
[HttpGet("loginRedirect")]
[AllowAnyAudience]

public async Task<ActionResult> LoginRedirect(
string jwt, // This is required because auth looks for a jwt in the query string
string returnTo)
Expand All @@ -53,6 +47,7 @@ public async Task<ActionResult> LoginRedirect(
return await EmailLinkExpired();
}
}

await HttpContext.SignInAsync(User,
new AuthenticationProperties { IsPersistent = true });
return Redirect(returnTo);
Expand Down Expand Up @@ -87,6 +82,7 @@ public async Task<string> CompleteGoogleLogin(ClaimsPrincipal? principal, string
{
(authUser, userEntity) = await lexAuthService.GetUser(googleEmail);
}

if (authUser is null)
{
authUser = new LexAuthUser()
Expand All @@ -102,19 +98,20 @@ public async Task<string> CompleteGoogleLogin(ClaimsPrincipal? principal, string
Locale = locale ?? LexCore.Entities.User.DefaultLocalizationCode,
Locked = null,
};
var queryParams = new Dictionary<string, string?>() {
{"email", googleEmail},
{"name", googleName},
{"returnTo", returnTo},
var queryParams = new Dictionary<string, string?>()
{
{ "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;
Expand Down Expand Up @@ -158,6 +155,7 @@ public async Task<ActionResult<LexAuthUser>> VerifyEmail(
return Redirect(returnTo);
}


[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
Expand Down Expand Up @@ -219,7 +217,9 @@ public async Task<ActionResult> 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)]
Expand Down
Loading

0 comments on commit e3d7d16

Please sign in to comment.