Disable password auth setting
This commit is contained in:
parent
1180d518a2
commit
188020597c
20 changed files with 164 additions and 75 deletions
|
@ -52,6 +52,7 @@ public class AccountController : BaseApiController
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly IOidcService _oidcService;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public AccountController(UserManager<AppUser> userManager,
|
public AccountController(UserManager<AppUser> userManager,
|
||||||
|
@ -60,7 +61,8 @@ public class AccountController : BaseApiController
|
||||||
ILogger<AccountController> logger,
|
ILogger<AccountController> logger,
|
||||||
IMapper mapper, IAccountService accountService,
|
IMapper mapper, IAccountService accountService,
|
||||||
IEmailService emailService, IEventHub eventHub,
|
IEmailService emailService, IEventHub eventHub,
|
||||||
ILocalizationService localizationService)
|
ILocalizationService localizationService,
|
||||||
|
IOidcService oidcService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_signInManager = signInManager;
|
_signInManager = signInManager;
|
||||||
|
@ -72,6 +74,7 @@ public class AccountController : BaseApiController
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
_oidcService = oidcService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
@ -80,6 +83,9 @@ public class AccountController : BaseApiController
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.UserPreferences);
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.UserPreferences);
|
||||||
if (user == null) throw new UnauthorizedAccessException();
|
if (user == null) throw new UnauthorizedAccessException();
|
||||||
|
|
||||||
|
var oidcSettings = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig;
|
||||||
|
await _oidcService.SyncUserSettings(oidcSettings, User, user);
|
||||||
|
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
||||||
|
|
||||||
|
@ -236,6 +242,11 @@ public class AccountController : BaseApiController
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
if (!roles.Contains(PolicyConstants.LoginRole)) return Unauthorized(await _localizationService.Translate(user.Id, "disabled-account"));
|
||||||
|
|
||||||
|
var oidcConfig = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig;
|
||||||
|
// Setting only takes effect if OIDC is funcitonal, and if we're not logging in via ApiKey
|
||||||
|
var disablePasswordAuthentication = oidcConfig is {Enabled: true, DisablePasswordAuthentication: true} && string.IsNullOrEmpty(loginDto.ApiKey);
|
||||||
|
if (disablePasswordAuthentication && !roles.Contains(PolicyConstants.AdminRole)) return Unauthorized(await _localizationService.Translate(user.Id, "password-authentication-disabled"));
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(loginDto.ApiKey))
|
if (string.IsNullOrEmpty(loginDto.ApiKey))
|
||||||
{
|
{
|
||||||
var result = await _signInManager
|
var result = await _signInManager
|
||||||
|
|
|
@ -1,32 +1,27 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs.Settings;
|
namespace API.DTOs.Settings;
|
||||||
|
|
||||||
public class OidcConfigDto
|
public class OidcConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc cref="ServerSettingKey.OidcAuthority"/>
|
||||||
/// Base url for authority, must have /.well-known/openid-configuration
|
|
||||||
/// </summary>
|
|
||||||
public string? Authority { get; set; }
|
public string? Authority { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.OidcClientId"/>
|
||||||
|
public string? ClientId { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.OidcProvisionAccounts"/>
|
||||||
|
public bool ProvisionAccounts { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.OidcRequireVerifiedEmail"/>
|
||||||
|
public bool RequireVerifiedEmail { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.OidcProvisionUserSettings"/>
|
||||||
|
public bool ProvisionUserSettings { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.OidcAutoLogin"/>
|
||||||
|
public bool AutoLogin { get; set; }
|
||||||
|
/// <inheritdoc cref="ServerSettingKey.DisablePasswordAuthentication"/>
|
||||||
|
public bool DisablePasswordAuthentication { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ClientId configured in your OpenID Connect provider
|
/// Returns true if the <see cref="Authority"/> has been set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ClientId { get; set; }
|
public bool Enabled => Authority != "";
|
||||||
/// <summary>
|
|
||||||
/// Create a new account when someone logs in with an unmatched account, if <see cref="RequireVerifiedEmail"/> is true,
|
|
||||||
/// will account will be verified by default
|
|
||||||
/// </summary>
|
|
||||||
public bool ProvisionAccounts { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Require emails from OpenIDConnect to be verified before use
|
|
||||||
/// </summary>
|
|
||||||
public bool RequireVerifiedEmail { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Overwrite Kavita roles, libraries and age rating with OpenIDConnect provides roles on log in.
|
|
||||||
/// </summary>
|
|
||||||
public bool ProvisionUserSettings { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Try logging in automatically when opening the app
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoLogin { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,6 @@ public sealed record OidcPublicConfigDto
|
||||||
public string? ClientId { get; set; }
|
public string? ClientId { get; set; }
|
||||||
/// <inheritdoc cref="OidcConfigDto.AutoLogin"/>
|
/// <inheritdoc cref="OidcConfigDto.AutoLogin"/>
|
||||||
public bool AutoLogin { get; set; }
|
public bool AutoLogin { get; set; }
|
||||||
|
/// <inheritdoc cref="OidcConfigDto.DisablePasswordAuthentication"/>
|
||||||
|
public bool DisablePasswordAuthentication { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -258,6 +258,7 @@ public static class Seed
|
||||||
new() { Key = ServerSettingKey.OidcProvisionAccounts, Value = "false"},
|
new() { Key = ServerSettingKey.OidcProvisionAccounts, Value = "false"},
|
||||||
new() { Key = ServerSettingKey.OidcRequireVerifiedEmail, Value = "true"},
|
new() { Key = ServerSettingKey.OidcRequireVerifiedEmail, Value = "true"},
|
||||||
new() { Key = ServerSettingKey.OidcProvisionUserSettings, Value = "false"},
|
new() { Key = ServerSettingKey.OidcProvisionUserSettings, Value = "false"},
|
||||||
|
new() { Key = ServerSettingKey.DisablePasswordAuthentication, Value = "false"},
|
||||||
|
|
||||||
new() {Key = ServerSettingKey.EmailHost, Value = string.Empty},
|
new() {Key = ServerSettingKey.EmailHost, Value = string.Empty},
|
||||||
new() {Key = ServerSettingKey.EmailPort, Value = string.Empty},
|
new() {Key = ServerSettingKey.EmailPort, Value = string.Empty},
|
||||||
|
|
|
@ -227,4 +227,9 @@ public enum ServerSettingKey
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("OpenIDConnectSyncUserSettings")]
|
[Description("OpenIDConnectSyncUserSettings")]
|
||||||
OidcProvisionUserSettings = 45,
|
OidcProvisionUserSettings = 45,
|
||||||
|
/// <summary>
|
||||||
|
/// Disables password authentication for non-admin users
|
||||||
|
/// </summary>
|
||||||
|
[Description("DisablePasswordAuthentication")]
|
||||||
|
DisablePasswordAuthentication = 46,
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,10 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
||||||
destination.OidcConfig ??= new OidcConfigDto();
|
destination.OidcConfig ??= new OidcConfigDto();
|
||||||
destination.OidcConfig.ProvisionUserSettings = bool.Parse(row.Value);
|
destination.OidcConfig.ProvisionUserSettings = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.DisablePasswordAuthentication:
|
||||||
|
destination.OidcConfig ??= new OidcConfigDto();
|
||||||
|
destination.OidcConfig.DisablePasswordAuthentication = bool.Parse(row.Value);
|
||||||
|
break;
|
||||||
case ServerSettingKey.LicenseKey:
|
case ServerSettingKey.LicenseKey:
|
||||||
case ServerSettingKey.EnableAuthentication:
|
case ServerSettingKey.EnableAuthentication:
|
||||||
case ServerSettingKey.EmailServiceUrl:
|
case ServerSettingKey.EmailServiceUrl:
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"confirm-email": "You must confirm your email first",
|
"confirm-email": "You must confirm your email first",
|
||||||
"locked-out": "You've been locked out from too many authorization attempts. Please wait 10 minutes.",
|
"locked-out": "You've been locked out from too many authorization attempts. Please wait 10 minutes.",
|
||||||
"disabled-account": "Your account is disabled. Contact the server admin.",
|
"disabled-account": "Your account is disabled. Contact the server admin.",
|
||||||
|
"password-authentication-disabled": "Password authentication has been disabled, login via OpenID Connect",
|
||||||
"register-user": "Something went wrong when registering user",
|
"register-user": "Something went wrong when registering user",
|
||||||
"validate-email": "There was an issue validating your email: {0}",
|
"validate-email": "There was an issue validating your email: {0}",
|
||||||
"confirm-token-gen": "There was an issue generating a confirmation token",
|
"confirm-token-gen": "There was an issue generating a confirmation token",
|
||||||
|
|
|
@ -28,6 +28,13 @@ public interface IOidcService
|
||||||
/// <exception cref="KavitaException">if any requirements aren't met</exception>
|
/// <exception cref="KavitaException">if any requirements aren't met</exception>
|
||||||
Task<AppUser?> LoginOrCreate(ClaimsPrincipal principal);
|
Task<AppUser?> LoginOrCreate(ClaimsPrincipal principal);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Updates roles, library access and age rating. Does not assign admin role, or to admin roles
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings"></param>
|
||||||
|
/// <param name="claimsPrincipal"></param>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
Task SyncUserSettings(OidcConfigDto settings, ClaimsPrincipal claimsPrincipal, AppUser user);
|
||||||
|
/// <summary>
|
||||||
/// Remove <see cref="AppUser.ExternalId"/> from all users
|
/// Remove <see cref="AppUser.ExternalId"/> from all users
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
@ -51,7 +58,6 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
||||||
var user = await unitOfWork.UserRepository.GetByExternalId(externalId, AppUserIncludes.UserPreferences);
|
var user = await unitOfWork.UserRepository.GetByExternalId(externalId, AppUserIncludes.UserPreferences);
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
// await SyncUserSettings(settings, principal, user);
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,14 +141,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task SyncUserSettings(OidcConfigDto settings, ClaimsPrincipal claimsPrincipal, AppUser user)
|
||||||
/// Updates roles, library access and age rating. Does not assign admin role, or to admin roles
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="settings"></param>
|
|
||||||
/// <param name="claimsPrincipal"></param>
|
|
||||||
/// <param name="user"></param>
|
|
||||||
/// <remarks>Extra feature, little buggy for now</remarks>
|
|
||||||
private async Task SyncUserSettings(OidcConfigDto settings, ClaimsPrincipal claimsPrincipal, AppUser user)
|
|
||||||
{
|
{
|
||||||
if (!settings.ProvisionUserSettings) return;
|
if (!settings.ProvisionUserSettings) return;
|
||||||
|
|
||||||
|
@ -196,7 +195,7 @@ public class OidcService(ILogger<OidcService> logger, UserManager<AppUser> userM
|
||||||
.ToList();
|
.ToList();
|
||||||
if (ageRatings.Count == 0) return;
|
if (ageRatings.Count == 0) return;
|
||||||
|
|
||||||
var highestAgeRating = AgeRating.NotApplicable;
|
var highestAgeRating = AgeRating.Unknown;
|
||||||
|
|
||||||
foreach (var ar in ageRatings)
|
foreach (var ar in ageRatings)
|
||||||
{
|
{
|
||||||
|
|
|
@ -366,8 +366,10 @@ public class SettingsService : ISettingsService
|
||||||
var url = authority + "/.well-known/openid-configuration";
|
var url = authority + "/.well-known/openid-configuration";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await url.GetJsonAsync<OpenIdConnectConfiguration>();
|
//await url.GetJsonAsync<OpenIdConnectConfiguration>();
|
||||||
return true;
|
//return true;
|
||||||
|
var res = await url.GetAsync();
|
||||||
|
return res.StatusCode == 200;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -455,6 +457,13 @@ public class SettingsService : ISettingsService
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.DisablePasswordAuthentication &&
|
||||||
|
updateSettingsDto.OidcConfig.DisablePasswordAuthentication + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = updateSettingsDto.OidcConfig.DisablePasswordAuthentication + string.Empty;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {UserUpdateEvent} from '../_models/events/user-update-event';
|
||||||
import {AgeRating} from '../_models/metadata/age-rating';
|
import {AgeRating} from '../_models/metadata/age-rating';
|
||||||
import {AgeRestriction} from '../_models/metadata/age-restriction';
|
import {AgeRestriction} from '../_models/metadata/age-restriction';
|
||||||
import {TextResonse} from '../_types/text-response';
|
import {TextResonse} from '../_types/text-response';
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
|
||||||
import {Action} from "./action-factory.service";
|
import {Action} from "./action-factory.service";
|
||||||
import {LicenseService} from "./license.service";
|
import {LicenseService} from "./license.service";
|
||||||
import {LocalizationService} from "./localization.service";
|
import {LocalizationService} from "./localization.service";
|
||||||
|
@ -63,6 +63,8 @@ export class AccountService {
|
||||||
return this.hasAdminRole(u);
|
return this.hasAdminRole(u);
|
||||||
}), shareReplay({bufferSize: 1, refCount: true}));
|
}), shareReplay({bufferSize: 1, refCount: true}));
|
||||||
|
|
||||||
|
public currentUserSignal = toSignal(this.currentUserSource);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -91,8 +91,10 @@ export class OidcService {
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
if (this.token) {
|
||||||
this.oauth2.logOut();
|
this.oauth2.logOut();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config() {
|
config() {
|
||||||
return this.httpClient.get<OidcPublicConfig>(this.baseUrl + "oidc/config");
|
return this.httpClient.get<OidcPublicConfig>(this.baseUrl + "oidc/config");
|
||||||
|
|
|
@ -6,10 +6,12 @@ export interface OidcConfig {
|
||||||
requireVerifiedEmail: boolean;
|
requireVerifiedEmail: boolean;
|
||||||
provisionUserSettings: boolean;
|
provisionUserSettings: boolean;
|
||||||
autoLogin: boolean;
|
autoLogin: boolean;
|
||||||
|
disablePasswordAuthentication: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OidcPublicConfig {
|
export interface OidcPublicConfig {
|
||||||
authority: string;
|
authority: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
autoLogin: boolean;
|
autoLogin: boolean;
|
||||||
|
disablePasswordAuthentication: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,18 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4 mb-4">
|
||||||
|
@if(settingsForm.get('disablePasswordAuthentication'); as formControl) {
|
||||||
|
<app-setting-switch [title]="t('disablePasswordAuthentication')" [subtitle]="t('disablePasswordAuthentication-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input id="disablePasswordAuthentication" type="checkbox" class="form-check-input" formControlName="disablePasswordAuthentication">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -51,6 +51,7 @@ export class ManageOpenIDConnectComponent implements OnInit {
|
||||||
this.settingsForm.addControl('requireVerifiedEmail', new FormControl(this.oidcSettings.requireVerifiedEmail, []));
|
this.settingsForm.addControl('requireVerifiedEmail', new FormControl(this.oidcSettings.requireVerifiedEmail, []));
|
||||||
this.settingsForm.addControl('provisionUserSettings', new FormControl(this.oidcSettings.provisionUserSettings, []));
|
this.settingsForm.addControl('provisionUserSettings', new FormControl(this.oidcSettings.provisionUserSettings, []));
|
||||||
this.settingsForm.addControl('autoLogin', new FormControl(this.oidcSettings.autoLogin, []));
|
this.settingsForm.addControl('autoLogin', new FormControl(this.oidcSettings.autoLogin, []));
|
||||||
|
this.settingsForm.addControl('disablePasswordAuthentication', new FormControl(this.oidcSettings.disablePasswordAuthentication, []));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -103,7 +103,8 @@ export class AppComponent implements OnInit {
|
||||||
// Login automatically when a token is available
|
// Login automatically when a token is available
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const ready = this.oidcService.ready();
|
const ready = this.oidcService.ready();
|
||||||
if (!ready || !this.oidcService.token) return;
|
const user = this.accountService.currentUserSignal();
|
||||||
|
if (!ready || !this.oidcService.token || user) return;
|
||||||
|
|
||||||
this.accountService.loginByToken(this.oidcService.token).subscribe({
|
this.accountService.loginByToken(this.oidcService.token).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<ng-container *transloco="let t; read: 'login'">
|
<ng-container *transloco="let t; prefix: 'login'">
|
||||||
<app-splash-container>
|
<app-splash-container>
|
||||||
<ng-container title><h2>{{t('title')}}</h2></ng-container>
|
<ng-container title><h2>{{t('title')}}</h2></ng-container>
|
||||||
<ng-container body>
|
<ng-container body>
|
||||||
<ng-container *ngIf="isLoaded()">
|
|
||||||
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow()">
|
@if (isLoaded()) {
|
||||||
|
@if (showPasswordLogin() && !firstTimeFlow()) {
|
||||||
|
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation">
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label visually-hidden">{{t('username')}}</label>
|
<label for="username" class="form-label visually-hidden">{{t('username')}}</label>
|
||||||
|
@ -26,12 +28,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@if (oidcService.ready()) {
|
|
||||||
<button [ngbTooltip]="t('oidc-tooltip')" class="btn btn-outline-primary mt-2" (click)="oidcService.login()">{{t('oidc')}}</button>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</ng-container>
|
@if (oidcService.ready()) {
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-primary mt-2 d-flex align-items-center gap-2"
|
||||||
|
(click)="oidcService.login()">
|
||||||
|
<img
|
||||||
|
ngSrc="assets/icons/open-id-connect-logo.svg"
|
||||||
|
alt="OIDC"
|
||||||
|
width="36"
|
||||||
|
height="36"
|
||||||
|
class="d-inline-block"
|
||||||
|
style="object-fit: contain;"
|
||||||
|
/>
|
||||||
|
{{t('oidc')}}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!showPasswordLogin()) {
|
||||||
|
<div class="text-muted mt-2">
|
||||||
|
{{t('bypass')}}
|
||||||
|
<span class="text-muted clickable" (click)="forceShowPasswordLogin.set(true)">{{t('here')}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</app-splash-container>
|
</app-splash-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -46,3 +46,7 @@ a {
|
||||||
font-family: var(--login-input-font-family);
|
font-family: var(--login-input-font-family);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +1,24 @@
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component, computed,
|
Component, computed,
|
||||||
DestroyRef, effect, inject,
|
effect, inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||||
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
|
||||||
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { AccountService } from '../../_services/account.service';
|
import { AccountService } from '../../_services/account.service';
|
||||||
import { MemberService } from '../../_services/member.service';
|
import { MemberService } from '../../_services/member.service';
|
||||||
import { NavService } from '../../_services/nav.service';
|
import { NavService } from '../../_services/nav.service';
|
||||||
import { NgIf } from '@angular/common';
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
import { SplashContainerComponent } from '../_components/splash-container/splash-container.component';
|
import { SplashContainerComponent } from '../_components/splash-container/splash-container.component';
|
||||||
import {TRANSLOCO_SCOPE, TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {environment} from "../../../environments/environment";
|
import {environment} from "../../../environments/environment";
|
||||||
import {OidcService} from "../../_services/oidc.service";
|
import {OidcService} from "../../_services/oidc.service";
|
||||||
import {forkJoin} from "rxjs";
|
|
||||||
import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -29,7 +26,7 @@ import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
|
||||||
templateUrl: './user-login.component.html',
|
templateUrl: './user-login.component.html',
|
||||||
styleUrls: ['./user-login.component.scss'],
|
styleUrls: ['./user-login.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [SplashContainerComponent, NgIf, ReactiveFormsModule, RouterLink, TranslocoDirective, NgbTooltip]
|
imports: [SplashContainerComponent, ReactiveFormsModule, RouterLink, TranslocoDirective, NgbTooltip, NgOptimizedImage]
|
||||||
})
|
})
|
||||||
export class UserLoginComponent implements OnInit {
|
export class UserLoginComponent implements OnInit {
|
||||||
|
|
||||||
|
@ -61,7 +58,23 @@ export class UserLoginComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* undefined until query params are read
|
* undefined until query params are read
|
||||||
*/
|
*/
|
||||||
skipAutoLogin = signal<boolean | undefined>(undefined)
|
skipAutoLogin = signal<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the login form, regardless if the password authentication is disabled (admins can still log in)
|
||||||
|
*/
|
||||||
|
forceShowPasswordLogin = signal(false);
|
||||||
|
/**
|
||||||
|
* Display the login form
|
||||||
|
*/
|
||||||
|
showPasswordLogin = computed(() => {
|
||||||
|
const loaded = this.isLoaded();
|
||||||
|
const config = this.oidcService.settings();
|
||||||
|
const force = this.forceShowPasswordLogin();
|
||||||
|
if (force) return true;
|
||||||
|
|
||||||
|
return loaded && config && !config.disablePasswordAuthentication;
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
|
|
1
UI/Web/src/assets/icons/open-id-connect-logo.svg
Normal file
1
UI/Web/src/assets/icons/open-id-connect-logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0"?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="_x32_39-openid"><g><path d="M234.849,419v6.623c-79.268-9.958-139.334-53.393-139.334-105.757 c0-39.313,33.873-73.595,84.485-92.511L178.023,180C88.892,202.497,26.001,256.607,26.001,319.866 c0,76.288,90.871,139.128,208.95,149.705l0.018-0.009V419H234.849z" style="fill:#B2B2B2;"/><polygon points="304.772,436.713 304.67,436.713 304.67,221.667 304.67,213.667 304.67,42.429 234.849,78.25 234.849,221.667 234.969,221.667 234.969,469.563 " style="fill:#F7931E;"/><path d="M485.999,291.938l-9.446-100.114l-35.938,20.331C415.087,196.649,382.5,177.5,340,177.261 l0.002,36.406v7.498c3.502,0.968,6.923,2.024,10.301,3.125c14.145,4.611,27.176,10.352,38.666,17.128l-37.786,21.254 L485.999,291.938z" style="fill:#B2B2B2;"/></g></g><g id="Layer_1"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -7,7 +7,8 @@
|
||||||
"forgot-password": "Forgot Password?",
|
"forgot-password": "Forgot Password?",
|
||||||
"submit": "Sign in",
|
"submit": "Sign in",
|
||||||
"oidc": "Log in with OpenID Connect",
|
"oidc": "Log in with OpenID Connect",
|
||||||
"oidc-tooltip": "This will connect you to an external site"
|
"bypass": "Password login has been disabled. Bypass ",
|
||||||
|
"here": "here"
|
||||||
},
|
},
|
||||||
|
|
||||||
"oidc": {
|
"oidc": {
|
||||||
|
@ -34,7 +35,9 @@
|
||||||
"provisionUserSettings": "Provision user settings",
|
"provisionUserSettings": "Provision user settings",
|
||||||
"provisionUserSettings-tooltip": "Synchronise Kavita user settings (roles, libraries, age rating) with those provided by the OIDC. See documentation for more information",
|
"provisionUserSettings-tooltip": "Synchronise Kavita user settings (roles, libraries, age rating) with those provided by the OIDC. See documentation for more information",
|
||||||
"autoLogin": "Auto login",
|
"autoLogin": "Auto login",
|
||||||
"autoLogin-tooltip": "Auto redirect to OpenID Connect provider when opening the login screen"
|
"autoLogin-tooltip": "Auto redirect to OpenID Connect provider when opening the login screen",
|
||||||
|
"disablePasswordAuthentication": "Disable password authentication",
|
||||||
|
"disablePasswordAuthentication-tooltip": "Users with the admin role can bypass this restriction"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue