Authority url validator
This commit is contained in:
parent
5480df4cfb
commit
1180d518a2
9 changed files with 103 additions and 16 deletions
|
@ -27,7 +27,7 @@ public class SettingsServiceTests
|
|||
_mockUnitOfWork = Substitute.For<IUnitOfWork>();
|
||||
_settingsService = new SettingsService(_mockUnitOfWork, ds,
|
||||
Substitute.For<ILibraryWatcher>(), Substitute.For<ITaskScheduler>(),
|
||||
Substitute.For<ILogger<SettingsService>>());
|
||||
Substitute.For<ILogger<SettingsService>>(), Substitute.For<IOidcService>());
|
||||
}
|
||||
|
||||
#region UpdateMetadataSettings
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Settings;
|
||||
using API.Services;
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -8,10 +9,11 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace API.Controllers;
|
||||
|
||||
[AllowAnonymous]
|
||||
public class OidcController(ILogger<OidcController> logger, IUnitOfWork unitOfWork, IMapper mapper): BaseApiController
|
||||
public class OidcController(ILogger<OidcController> logger, IUnitOfWork unitOfWork,
|
||||
IMapper mapper, ISettingsService settingsService): BaseApiController
|
||||
{
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("config")]
|
||||
public async Task<ActionResult<OidcPublicConfigDto>> GetOidcConfig()
|
||||
{
|
||||
|
@ -19,4 +21,16 @@ public class OidcController(ILogger<OidcController> logger, IUnitOfWork unitOfWo
|
|||
return Ok(mapper.Map<OidcPublicConfigDto>(settings.OidcConfig));
|
||||
}
|
||||
|
||||
[Authorize("RequireAdminRole")]
|
||||
[HttpPost("is-valid-authority")]
|
||||
public async Task<ActionResult<bool>> IsValidAuthority([FromBody] IsValidAuthorityBody authority)
|
||||
{
|
||||
return Ok(await settingsService.IsValidAuthority(authority.Authority));
|
||||
}
|
||||
|
||||
public class IsValidAuthorityBody
|
||||
{
|
||||
public string Authority { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ public static class IdentityServiceExtensions
|
|||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = SetTokenFromQuery
|
||||
OnMessageReceived = SetTokenFromQuery,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -164,8 +164,12 @@ public static class IdentityServiceExtensions
|
|||
if (ctx.Principal == null) return;
|
||||
|
||||
var user = await oidcService.LoginOrCreate(ctx.Principal);
|
||||
if (user == null) return;
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
ctx.Principal = null;
|
||||
await ctx.HttpContext.SignOutAsync(OpenIdConnect);
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
|
|
|
@ -27,6 +27,11 @@ public interface IOidcService
|
|||
/// <returns></returns>
|
||||
/// <exception cref="KavitaException">if any requirements aren't met</exception>
|
||||
Task<AppUser?> LoginOrCreate(ClaimsPrincipal principal);
|
||||
/// <summary>
|
||||
/// Remove <see cref="AppUser.ExternalId"/> from all users
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ClearOidcIds();
|
||||
}
|
||||
|
||||
public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userManager,
|
||||
|
@ -46,7 +51,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
var user = await unitOfWork.UserRepository.GetByExternalId(externalId, AppUserIncludes.UserPreferences);
|
||||
if (user != null)
|
||||
{
|
||||
//await SyncUserSettings(settings, principal, user);
|
||||
// await SyncUserSettings(settings, principal, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -64,7 +69,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
|
||||
user.ExternalId = externalId;
|
||||
|
||||
//await SyncUserSettings(settings, principal, user);
|
||||
await SyncUserSettings(settings, principal, user);
|
||||
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
if (roles.Count > 0 && !roles.Contains(PolicyConstants.LoginRole))
|
||||
|
@ -73,6 +78,17 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
return user;
|
||||
}
|
||||
|
||||
public async Task ClearOidcIds()
|
||||
{
|
||||
var users = await unitOfWork.UserRepository.GetAllUsersAsync();
|
||||
foreach (var user in users)
|
||||
{
|
||||
user.ExternalId = null;
|
||||
}
|
||||
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
private async Task<AppUser?> NewUserFromOpenIdConnect(OidcConfigDto settings, ClaimsPrincipal claimsPrincipal)
|
||||
{
|
||||
if (!settings.ProvisionAccounts) return null;
|
||||
|
@ -133,10 +149,12 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
|||
var userRoles = await userManager.GetRolesAsync(user);
|
||||
if (userRoles.Contains(PolicyConstants.AdminRole)) return;
|
||||
|
||||
|
||||
await SyncRoles(claimsPrincipal, user);
|
||||
await SyncLibraries(claimsPrincipal, user);
|
||||
SyncAgeRating(claimsPrincipal, user);
|
||||
|
||||
|
||||
if (unitOfWork.HasChanges())
|
||||
await unitOfWork.CommitAsync();
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ using Kavita.Common.EnvironmentInfo;
|
|||
using Kavita.Common.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
|
@ -24,6 +25,12 @@ public interface ISettingsService
|
|||
{
|
||||
Task<MetadataSettingsDto> UpdateMetadataSettings(MetadataSettingsDto dto);
|
||||
Task<ServerSettingDto> UpdateSettings(ServerSettingDto updateSettingsDto);
|
||||
/// <summary>
|
||||
/// Check if the server can reach the authority at the given uri
|
||||
/// </summary>
|
||||
/// <param name="authority"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> IsValidAuthority(string authority);
|
||||
}
|
||||
|
||||
|
||||
|
@ -34,16 +41,18 @@ public class SettingsService : ISettingsService
|
|||
private readonly ILibraryWatcher _libraryWatcher;
|
||||
private readonly ITaskScheduler _taskScheduler;
|
||||
private readonly ILogger<SettingsService> _logger;
|
||||
private readonly IOidcService _oidcService;
|
||||
|
||||
public SettingsService(IUnitOfWork unitOfWork, IDirectoryService directoryService,
|
||||
ILibraryWatcher libraryWatcher, ITaskScheduler taskScheduler,
|
||||
ILogger<SettingsService> logger)
|
||||
ILogger<SettingsService> logger, IOidcService oidcService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_libraryWatcher = libraryWatcher;
|
||||
_taskScheduler = taskScheduler;
|
||||
_logger = logger;
|
||||
_oidcService = oidcService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -347,7 +356,7 @@ public class SettingsService : ISettingsService
|
|||
return updateSettingsDto;
|
||||
}
|
||||
|
||||
private async Task<bool> IsValidAuthority(string authority)
|
||||
public async Task<bool> IsValidAuthority(string authority)
|
||||
{
|
||||
if (string.IsNullOrEmpty(authority))
|
||||
{
|
||||
|
@ -357,8 +366,8 @@ public class SettingsService : ISettingsService
|
|||
var url = authority + "/.well-known/openid-configuration";
|
||||
try
|
||||
{
|
||||
var resp = await url.GetAsync();
|
||||
return resp.StatusCode == 200;
|
||||
await url.GetJsonAsync<OpenIdConnectConfiguration>();
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -413,6 +422,8 @@ public class SettingsService : ISettingsService
|
|||
setting.Value = updateSettingsDto.OidcConfig.Authority + string.Empty;
|
||||
Configuration.OidcAuthority = setting.Value;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
|
||||
await _oidcService.ClearOidcIds();
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.OidcClientId &&
|
||||
|
|
|
@ -21,6 +21,14 @@
|
|||
<input id="oid-authority" class="form-control"
|
||||
formControlName="authority" type="text"
|
||||
[class.is-invalid]="formControl.invalid && !formControl.untouched">
|
||||
|
||||
@if (settingsForm.dirty || !settingsForm.untouched) {
|
||||
<div id="oidc-authority-validations" class="invalid-feedback">
|
||||
@if (formControl.errors?.invalidUri) {
|
||||
<div>{{t('invalidUri')}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import {ChangeDetectorRef, Component, OnInit} from '@angular/core';
|
|||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ServerSettings} from "../_models/server-settings";
|
||||
import {
|
||||
AbstractControl,
|
||||
AsyncValidatorFn,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
|
@ -12,6 +14,7 @@ import {SettingsService} from "../settings.service";
|
|||
import {OidcConfig} from "../_models/oidc-config";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
|
||||
import {map, of} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-open-idconnect',
|
||||
|
@ -42,9 +45,7 @@ export class ManageOpenIDConnectComponent implements OnInit {
|
|||
this.serverSettings = data;
|
||||
this.oidcSettings = this.serverSettings.oidcConfig;
|
||||
|
||||
|
||||
// TODO: Validator for authority, /.well-known/openid-configuration endpoint must be reachable
|
||||
this.settingsForm.addControl('authority', new FormControl(this.oidcSettings.authority, []));
|
||||
this.settingsForm.addControl('authority', new FormControl(this.oidcSettings.authority, [], [this.authorityValidator()]));
|
||||
this.settingsForm.addControl('clientId', new FormControl(this.oidcSettings.clientId, [this.requiredIf('authority')]));
|
||||
this.settingsForm.addControl('provisionAccounts', new FormControl(this.oidcSettings.provisionAccounts, []));
|
||||
this.settingsForm.addControl('requireVerifiedEmail', new FormControl(this.oidcSettings.requireVerifiedEmail, []));
|
||||
|
@ -72,6 +73,31 @@ export class ManageOpenIDConnectComponent implements OnInit {
|
|||
})
|
||||
}
|
||||
|
||||
authorityValidator(): AsyncValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
let uri: string = control.value;
|
||||
if (!uri || uri.trim().length === 0) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(uri);
|
||||
} catch {
|
||||
return of({'invalidUri': {'uri': uri}} as ValidationErrors)
|
||||
}
|
||||
|
||||
if (uri.endsWith('/')) {
|
||||
uri = uri.substring(0, uri.length - 1);
|
||||
}
|
||||
|
||||
return this.settingsService.ifValidAuthority(uri).pipe(map(ok => {
|
||||
if (ok) return null;
|
||||
|
||||
return {'invalidUri': {'uri': uri}} as ValidationErrors;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
requiredIf(other: string): ValidatorFn {
|
||||
return (control): ValidationErrors | null => {
|
||||
const otherControl = this.settingsForm.get(other);
|
||||
|
|
|
@ -78,6 +78,11 @@ export class SettingsService {
|
|||
isValidCronExpression(val: string) {
|
||||
if (val === '' || val === undefined || val === null) return of(false);
|
||||
return this.http.get<string>(this.baseUrl + 'settings/is-valid-cron?cronExpression=' + val, TextResonse).pipe(map(d => d === 'true'));
|
||||
}
|
||||
|
||||
ifValidAuthority(authority: string) {
|
||||
if (authority === '' || authority === undefined || authority === null) return of(false);
|
||||
|
||||
return this.http.post<boolean>(this.baseUrl + 'oidc/is-valid-authority', {authority});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
"settings": {
|
||||
"save": "{{common.save}}",
|
||||
"notice": "Notice",
|
||||
"restart-required": "Changing OpenID Connect settings requires a restart",
|
||||
"restart-required": "Changing OpenID Connect Authority requires a restart",
|
||||
"provider": "Provider",
|
||||
"behaviour": "Behaviour",
|
||||
"field-required": "{{name}} is required when {{other}} is set",
|
||||
"invalidUri": "The provider URL is not valid",
|
||||
|
||||
"authority": "Authority",
|
||||
"authority-tooltip": "The URL to your OpenID Connect provider",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue