2 Commits aa77282e59 ... 3e66c04c43

Author SHA1 Message Date
  Owen Diffey 3e66c04c43 refactor: Fetches OIDC endpoints once during users module initialization 2 months ago
  Owen Diffey 36e77dc931 feat: Disable registration on frontend and bypass login modal for OIDC 2 months ago

+ 2 - 13
backend/logic/app.js

@@ -4,7 +4,6 @@ import cookieParser from "cookie-parser";
 import bodyParser from "body-parser";
 import express from "express";
 import http from "http";
-import axios from "axios";
 
 import CoreClass from "../core";
 
@@ -145,16 +144,6 @@ class _AppModule extends CoreClass {
 		}
 
 		if (config.get("apis.oidc.enabled")) {
-			const redirectUri =
-				config.get("apis.oidc.redirect_uri").length > 0
-					? config.get("apis.oidc.redirect_uri")
-					: `${appUrl}/backend/auth/oidc/authorize/callback`;
-
-			// TODO don't fetch the openid configuration twice (app module and user module)
-			const openidConfigurationResponse = await axios.get(config.get("apis.oidc.openid_configuration_url"));
-
-			const { authorization_endpoint: authorizationEndpoint } = openidConfigurationResponse.data;
-
 			app.get("/auth/oidc/authorize", async (req, res) => {
 				if (this.getStatus() !== "READY") {
 					this.log(
@@ -167,11 +156,11 @@ class _AppModule extends CoreClass {
 
 				const params = [
 					`client_id=${config.get("apis.oidc.client_id")}`,
-					`redirect_uri=${redirectUri}`,
+					`redirect_uri=${UsersModule.oidcRedirectUri}`,
 					`scope=basic openid`, // TODO check if openid is necessary for us
 					`response_type=code`
 				].join("&");
-				return res.redirect(`${authorizationEndpoint}?${params}`);
+				return res.redirect(`${UsersModule.oidcAuthorizationEndpoint}?${params}`);
 			});
 
 			app.get("/auth/oidc/authorize/callback", async (req, res) => {

+ 13 - 7
backend/logic/users.js

@@ -59,10 +59,6 @@ class _UsersModule extends CoreClass {
 			config.get("apis.github.redirect_uri").length > 0
 				? config.get("apis.github.redirect_uri")
 				: `${this.appUrl}/backend/auth/github/authorize/callback`;
-		this.oidcRedirectUri =
-			config.get("apis.oidc.redirect_uri").length > 0
-				? config.get("apis.oidc.redirect_uri")
-				: `${this.appUrl}/backend/auth/oidc/authorize/callback`;
 
 		this.oauth2 = new OAuth2(
 			config.get("apis.github.client"),
@@ -85,11 +81,20 @@ class _UsersModule extends CoreClass {
 		if (config.get("apis.oidc.enabled")) {
 			const openidConfigurationResponse = await axios.get(config.get("apis.oidc.openid_configuration_url"));
 
-			const { token_endpoint: tokenEndpoint, userinfo_endpoint: userinfoEndpoint } =
-				openidConfigurationResponse.data;
+			const {
+				authorization_endpoint: authorizationEndpoint,
+				token_endpoint: tokenEndpoint,
+				userinfo_endpoint: userinfoEndpoint
+			} = openidConfigurationResponse.data;
 
 			// TODO somehow make this endpoint immutable, if possible in some way
+			this.oidcAuthorizationEndpoint = authorizationEndpoint;
+			this.oidcTokenEndpoint = userinfoEndpoint;
 			this.oidcUserinfoEndpoint = userinfoEndpoint;
+			this.oidcRedirectUri =
+				config.get("apis.oidc.redirect_uri").length > 0
+					? config.get("apis.oidc.redirect_uri")
+					: `${this.appUrl}/backend/auth/oidc/authorize/callback`;
 
 			//
 			const clientId = config.get("apis.oidc.client_id");
@@ -662,7 +667,8 @@ class _UsersModule extends CoreClass {
 		let { email } = payload;
 		email = email.toLowerCase().trim();
 
-		if (config.get("registrationDisabled") === true) throw new Error("Registration is not allowed at this time.");
+		if (config.get("registrationDisabled") === true || config.get("apis.oidc.enabled") === true)
+			throw new Error("Registration is not allowed at this time.");
 		if (Array.isArray(config.get("experimental.registration_email_whitelist"))) {
 			const experimentalRegistrationEmailWhitelist = config.get("experimental.registration_email_whitelist");
 

+ 3 - 2
backend/logic/ws.js

@@ -575,14 +575,15 @@ class _WSModule extends CoreClass {
 						enabled: config.get("apis.recaptcha.enabled"),
 						key: config.get("apis.recaptcha.key")
 					},
-					githubAuthentication: config.get("apis.github.enabled"),
+					githubAuthentication: config.get("apis.github.enabled") && !config.get("apis.oidc.enabled"),
 					oidcAuthentication: config.get("apis.oidc.enabled"),
 					messages: config.get("messages"),
 					christmas: config.get("christmas"),
 					footerLinks: config.get("footerLinks"),
 					primaryColor: config.get("primaryColor"),
 					shortcutOverrides: config.get("shortcutOverrides"),
-					registrationDisabled: config.get("registrationDisabled"),
+					registrationDisabled:
+						config.get("registrationDisabled") === true || config.get("apis.oidc.enabled") === true,
 					mailEnabled: config.get("mail.enabled"),
 					discogsEnabled: config.get("apis.discogs.enabled"),
 					experimental: {

+ 25 - 3
frontend/src/components/MainHeader.vue

@@ -2,6 +2,7 @@
 import { defineAsyncComponent, ref, onMounted, watch, nextTick } from "vue";
 import Toast from "toasters";
 import { storeToRefs } from "pinia";
+import { useRoute } from "vue-router";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useConfigStore } from "@/stores/config";
 import { useUserAuthStore } from "@/stores/userAuth";
@@ -18,6 +19,8 @@ defineProps({
 	hideLoggedOut: { type: Boolean, default: false }
 });
 
+const route = useRoute();
+
 const userAuthStore = useUserAuthStore();
 
 const localNightmode = ref(false);
@@ -28,8 +31,13 @@ const broadcastChannel = ref();
 const { socket } = useWebsocketsStore();
 
 const configStore = useConfigStore();
-const { cookie, sitename, registrationDisabled, christmas } =
-	storeToRefs(configStore);
+const {
+	cookie,
+	sitename,
+	registrationDisabled,
+	christmas,
+	oidcAuthentication
+} = storeToRefs(configStore);
 
 const { loggedIn, username } = storeToRefs(userAuthStore);
 const { logout, hasPermission } = userAuthStore;
@@ -59,6 +67,10 @@ const onResize = () => {
 	windowWidth.value = window.innerWidth;
 };
 
+const oidcRedirect = () => {
+	localStorage.setItem("oidc_redirect", route.path);
+};
+
 watch(nightmode, value => {
 	localNightmode.value = value;
 });
@@ -157,7 +169,17 @@ onMounted(async () => {
 				<a class="nav-item" @click="logout()">Logout</a>
 			</span>
 			<span v-if="!loggedIn && !hideLoggedOut" class="grouped">
-				<a class="nav-item" @click="openModal('login')">Login</a>
+				<a
+					v-if="oidcAuthentication"
+					class="nav-item"
+					:href="configStore.urls.api + '/auth/oidc/authorize'"
+					@click="oidcRedirect()"
+				>
+					Login
+				</a>
+				<a v-else class="nav-item" @click="openModal('login')">
+					Login
+				</a>
 				<a
 					v-if="!registrationDisabled"
 					class="nav-item"

+ 1 - 19
frontend/src/components/modals/Login.vue

@@ -19,8 +19,7 @@ const password = ref({
 const passwordElement = ref();
 
 const configStore = useConfigStore();
-const { githubAuthentication, oidcAuthentication, registrationDisabled } =
-	storeToRefs(configStore);
+const { githubAuthentication, registrationDisabled } = storeToRefs(configStore);
 const { login } = useUserAuthStore();
 
 const { openModal, closeCurrentModal } = useModalsStore();
@@ -67,9 +66,6 @@ const changeToRegisterModal = () => {
 const githubRedirect = () => {
 	localStorage.setItem("github_redirect", route.path);
 };
-const oidcRedirect = () => {
-	localStorage.setItem("oidc_redirect", route.path);
-};
 </script>
 
 <template>
@@ -168,20 +164,6 @@ const oidcRedirect = () => {
 						</div>
 						&nbsp;&nbsp;Login with GitHub
 					</a>
-					<a
-						v-if="oidcAuthentication"
-						class="button is-oidc"
-						:href="configStore.urls.api + '/auth/oidc/authorize'"
-						@click="oidcRedirect()"
-					>
-						<div class="icon">
-							<img
-								class="invert"
-								src="/assets/social/github.svg"
-							/>
-						</div>
-						&nbsp;&nbsp;Login with OIDC
-					</a>
 				</div>
 
 				<p

+ 2 - 24
frontend/src/components/modals/Register.vue

@@ -41,12 +41,8 @@ const passwordElement = ref();
 const { register } = useUserAuthStore();
 
 const configStore = useConfigStore();
-const {
-	registrationDisabled,
-	recaptcha,
-	githubAuthentication,
-	oidcAuthentication
-} = storeToRefs(configStore);
+const { registrationDisabled, recaptcha, githubAuthentication } =
+	storeToRefs(configStore);
 const { openModal, closeCurrentModal } = useModalsStore();
 
 const submitModal = () => {
@@ -84,10 +80,6 @@ const githubRedirect = () => {
 	localStorage.setItem("github_redirect", route.path);
 };
 
-const oidcRedirect = () => {
-	localStorage.setItem("oidc_redirect", route.path);
-};
-
 watch(
 	() => username.value.value,
 	value => {
@@ -296,20 +288,6 @@ onMounted(async () => {
 						</div>
 						&nbsp;&nbsp;Register with GitHub
 					</a>
-					<a
-						v-if="oidcAuthentication"
-						class="button is-oidc"
-						:href="configStore.urls.api + '/auth/oidc/authorize'"
-						@click="oidcRedirect()"
-					>
-						<div class="icon">
-							<img
-								class="invert"
-								src="/assets/social/github.svg"
-							/>
-						</div>
-						&nbsp;&nbsp;Register with OIDC
-					</a>
 				</div>
 
 				<p class="content-box-optional-helper">

+ 19 - 2
frontend/src/pages/Home.vue

@@ -38,7 +38,8 @@ const userAuthStore = useUserAuthStore();
 const route = useRoute();
 const router = useRouter();
 
-const { sitename, registrationDisabled } = storeToRefs(configStore);
+const { sitename, registrationDisabled, oidcAuthentication } =
+	storeToRefs(configStore);
 const { loggedIn, userId } = storeToRefs(userAuthStore);
 const { hasPermission } = userAuthStore;
 
@@ -152,6 +153,12 @@ const changeFavoriteOrder = ({ moved }) => {
 	);
 };
 
+const oidcRedirect = () => {
+	localStorage.setItem("oidc_redirect", route.path);
+
+	window.location.href = `${configStore.urls.api}/auth/oidc/authorize`;
+};
+
 watch(
 	() => hasPermission("stations.index.other"),
 	value => {
@@ -178,7 +185,9 @@ onMounted(async () => {
 	) {
 		// Makes sure the login/register modal isn't opened whenever the home page gets remounted due to a code change
 		handledLoginRegisterRedirect.value = true;
-		openModal(route.redirectedFrom.name);
+
+		if (oidcAuthentication.value) oidcRedirect();
+		else openModal(route.redirectedFrom.name);
 	}
 
 	socket.onConnect(() => {
@@ -384,7 +393,15 @@ onBeforeUnmount(() => {
 						/>
 						<span v-else class="logo">{{ sitename }}</span>
 						<div v-if="!loggedIn" class="buttons">
+							<a
+								v-if="oidcAuthentication"
+								class="button login"
+								@click="oidcRedirect()"
+							>
+								{{ t("Login") }}
+							</a>
 							<button
+								v-else
 								class="button login"
 								@click="openModal('login')"
 							>

+ 17 - 2
frontend/src/pages/Settings/Tabs/Account.vue

@@ -8,6 +8,7 @@ import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import { useModalsStore } from "@/stores/modals";
 import _validation from "@/validation";
+import { useConfigStore } from "@/stores/config";
 
 const InputHelpBox = defineAsyncComponent(
 	() => import("@/components/InputHelpBox.vue")
@@ -22,6 +23,7 @@ const QuickConfirm = defineAsyncComponent(
 const settingsStore = useSettingsStore();
 const userAuthStore = useUserAuthStore();
 const route = useRoute();
+const configStore = useConfigStore();
 
 const { socket } = useWebsocketsStore();
 
@@ -29,6 +31,7 @@ const saveButton = ref();
 
 const { userId } = storeToRefs(userAuthStore);
 const { originalUser, modifiedUser } = settingsStore;
+const { oidcAuthentication } = storeToRefs(configStore);
 
 const validation = reactive({
 	username: {
@@ -121,6 +124,8 @@ const changeUsername = () => {
 };
 
 const saveChanges = () => {
+	if (oidcAuthentication.value) return;
+
 	const usernameChanged = modifiedUser.username !== originalUser.username;
 	const emailAddressChanged =
 		modifiedUser.email.address !== originalUser.email.address;
@@ -224,13 +229,17 @@ watch(
 				autocomplete="off"
 				@keypress="onInput('username')"
 				@paste="onInput('username')"
+				:disabled="oidcAuthentication"
 			/>
-			<span v-if="modifiedUser.username" class="character-counter"
+			<span
+				v-if="modifiedUser.username && !oidcAuthentication"
+				class="character-counter"
 				>{{ modifiedUser.username.length }}/32</span
 			>
 		</p>
 		<transition name="fadein-helpbox">
 			<input-help-box
+				v-if="!oidcAuthentication"
 				:entered="validation.username.entered"
 				:valid="validation.username.valid"
 				:message="validation.username.message"
@@ -249,17 +258,23 @@ watch(
 				@keypress="onInput('email')"
 				@paste="onInput('email')"
 				autocomplete="off"
+				:disabled="oidcAuthentication"
 			/>
 		</p>
 		<transition name="fadein-helpbox">
 			<input-help-box
+				v-if="!oidcAuthentication"
 				:entered="validation.email.entered"
 				:valid="validation.email.valid"
 				:message="validation.email.message"
 			/>
 		</transition>
 
-		<SaveButton ref="saveButton" @clicked="saveChanges()" />
+		<SaveButton
+			v-if="!oidcAuthentication"
+			ref="saveButton"
+			@clicked="saveChanges()"
+		/>
 
 		<div class="section-margin-bottom" />
 

+ 9 - 1
frontend/src/pages/Settings/Tabs/Profile.vue

@@ -6,6 +6,7 @@ import { useSettingsStore } from "@/stores/settings";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useUserAuthStore } from "@/stores/userAuth";
 import validation from "@/validation";
+import { useConfigStore } from "@/stores/config";
 
 const ProfilePicture = defineAsyncComponent(
 	() => import("@/components/ProfilePicture.vue")
@@ -16,6 +17,7 @@ const SaveButton = defineAsyncComponent(
 
 const settingsStore = useSettingsStore();
 const userAuthStore = useUserAuthStore();
+const configStore = useConfigStore();
 
 const { socket } = useWebsocketsStore();
 
@@ -23,10 +25,13 @@ const saveButton = ref();
 
 const { userId } = storeToRefs(userAuthStore);
 const { originalUser, modifiedUser } = settingsStore;
+const { oidcAuthentication } = storeToRefs(configStore);
 
 const { updateOriginalUser } = settingsStore;
 
 const changeName = () => {
+	if (oidcAuthentication.value) return null;
+
 	modifiedUser.name = modifiedUser.name.replaceAll(/ +/g, " ").trim();
 	const { name } = modifiedUser;
 
@@ -211,8 +216,11 @@ const saveChanges = () => {
 				placeholder="Enter name here..."
 				maxlength="64"
 				v-model="modifiedUser.name"
+				:disabled="oidcAuthentication"
 			/>
-			<span v-if="modifiedUser.name" class="character-counter"
+			<span
+				v-if="modifiedUser.name && !oidcAuthentication"
+				class="character-counter"
 				>{{ modifiedUser.name.length }}/64</span
 			>
 		</p>

+ 9 - 2
frontend/src/pages/Settings/Tabs/Security.vue

@@ -16,7 +16,8 @@ const QuickConfirm = defineAsyncComponent(
 );
 
 const configStore = useConfigStore();
-const { githubAuthentication, sitename } = storeToRefs(configStore);
+const { githubAuthentication, sitename, oidcAuthentication } =
+	storeToRefs(configStore);
 const settingsStore = useSettingsStore();
 const userAuthStore = useUserAuthStore();
 
@@ -57,6 +58,8 @@ const onInput = inputName => {
 	validation[inputName].entered = true;
 };
 const changePassword = () => {
+	if (oidcAuthentication.value) return null;
+
 	const newPassword = validation.newPassword.value;
 
 	if (validation.oldPassword.value === "")
@@ -81,11 +84,15 @@ const changePassword = () => {
 	);
 };
 const unlinkPassword = () => {
+	if (oidcAuthentication.value) return;
+
 	socket.dispatch("users.unlinkPassword", res => {
 		new Toast(res.message);
 	});
 };
 const unlinkGitHub = () => {
+	if (!githubAuthentication.value) return;
+
 	socket.dispatch("users.unlinkGitHub", res => {
 		new Toast(res.message);
 	});
@@ -197,7 +204,7 @@ watch(validation, newValidation => {
 			<div class="section-margin-bottom" />
 		</div>
 
-		<div v-if="!isPasswordLinked">
+		<div v-if="!isPasswordLinked && !oidcAuthentication">
 			<h4 class="section-title">Add a password</h4>
 			<p class="section-description">
 				Add a password, as an alternative to signing in with GitHub