From ee7abadfea361621efcbe03002f79e6e353afade Mon Sep 17 00:00:00 2001 From: netpro2k Date: Thu, 12 Nov 2020 17:55:20 -0800 Subject: [PATCH 01/32] Support using displayName for signed in state --- src/react-components/auth/AuthContext.js | 8 ++++++-- src/react-components/layout/Header.js | 3 +-- src/react-components/presence-list.js | 5 ++--- src/react-components/ui-root.js | 11 ++++++++--- src/storage/store.js | 5 +++-- src/utils/auth-channel.js | 17 ++++++++--------- src/utils/phoenix-utils.js | 2 +- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/react-components/auth/AuthContext.js b/src/react-components/auth/AuthContext.js index e5344e785f..8f86d73cd3 100644 --- a/src/react-components/auth/AuthContext.js +++ b/src/react-components/auth/AuthContext.js @@ -1,6 +1,7 @@ import React, { createContext, useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import configs from "../../utils/configs"; +import maskEmail from "../../utils/mask-email"; // TODO: We really shouldn't include these dependencies on every page. A dynamic import would work better. import jwtDecode from "jwt-decode"; @@ -82,7 +83,8 @@ export function AuthContextProvider({ children, store }) { initialized: false, isSignedIn: !!store.state.credentials && store.state.credentials.token, isAdmin: configs.isAdmin(), - email: store.state.credentials && store.state.credentials.email, + displayName: + store.state.credentials && (store.state.credentials.displayName || maskEmail(store.state.credentials.email)), userId: store.credentialsAccountId, signIn, verify, @@ -97,7 +99,9 @@ export function AuthContextProvider({ children, store }) { ...state, isSignedIn: !!store.state.credentials && store.state.credentials.token, isAdmin: configs.isAdmin(), - email: store.state.credentials && store.state.credentials.email, + displayName: + store.state.credentials && + (store.state.credentials.displayName || maskEmail(store.state.credentials.email)), userId: store.credentialsAccountId })); }; diff --git a/src/react-components/layout/Header.js b/src/react-components/layout/Header.js index fd8cb8f6e6..cd1ffb8139 100644 --- a/src/react-components/layout/Header.js +++ b/src/react-components/layout/Header.js @@ -4,7 +4,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; import IfFeature from "../if-feature"; import configs from "../../utils/configs"; -import maskEmail from "../../utils/mask-email"; import styles from "./Header.scss"; import { AuthContext } from "../auth/AuthContext"; import { WrappedIntlProvider } from "../wrapped-intl-provider"; @@ -72,7 +71,7 @@ export function Header() { {auth.isSignedIn ? (
- {maskEmail(auth.email)} + {auth.displayName} {" "} diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index 67ee635942..260947cddc 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -7,7 +7,6 @@ import classNames from "classnames"; import rootStyles from "../assets/stylesheets/ui-root.scss"; import styles from "../assets/stylesheets/presence-list.scss"; -import maskEmail from "../utils/mask-email"; import StateLink from "./state-link.js"; import { WithHoverSound } from "./wrap-with-audio"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -67,7 +66,7 @@ export default class PresenceList extends Component { history: PropTypes.object, sessionId: PropTypes.string, signedIn: PropTypes.bool, - email: PropTypes.string, + displayName: PropTypes.string, onSignIn: PropTypes.func, onSignOut: PropTypes.func, expanded: PropTypes.bool, @@ -218,7 +217,7 @@ export default class PresenceList extends Component { {this.props.signedIn ? (
- {maskEmail(this.props.email)} + {this.props.displayName} {" "} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index addb9f90d6..1ebef20c5d 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -11,6 +11,7 @@ import IfFeature from "./if-feature"; import UnlessFeature from "./unless-feature"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { canShare } from "../utils/share"; +import maskEmail from "../utils/mask-email"; import styles from "../assets/stylesheets/ui-root.scss"; import entryStyles from "../assets/stylesheets/entry.scss"; import inviteStyles from "../assets/stylesheets/invite-dialog.scss"; @@ -868,8 +869,10 @@ class UIRoot extends Component { showSignInDialog = () => { this.showNonHistoriedDialog(SignInDialog, { message: getMessages()["sign-in.prompt"], - onSignIn: async email => { - const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel); + onSignIn: async authPayload => { + const { authComplete } = await (authPayload == "oidc" + ? this.props.authChannel.startOIDCAuthentication(this.props.hubChannel) + : this.props.authChannel.startAuthentication(authPayload, this.props.hubChannel)); this.showNonHistoriedDialog(SignInDialog, { authStarted: true }); @@ -2067,7 +2070,9 @@ class UIRoot extends Component { presences={this.props.presences} sessionId={this.props.sessionId} signedIn={this.state.signedIn} - email={this.props.store.state.credentials.email} + displayName={ + this.props.store.state.credentials.displayName || maskEmail(this.props.store.state.credentials.email) + } onSignIn={this.showSignInDialog} onSignOut={this.signOut} expanded={!this.state.isObjectListExpanded && this.state.isPresenceListExpanded} diff --git a/src/storage/store.js b/src/storage/store.js index d60041f813..cebf961579 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -56,7 +56,8 @@ export const SCHEMA = { additionalProperties: false, properties: { token: { type: ["null", "string"] }, - email: { type: ["null", "string"] } + email: { type: ["null", "string"] }, + displayName: { type: ["null", "string"] } } }, @@ -249,7 +250,7 @@ export default class Store extends EventTarget { const expiry = jwtDecode(this.state.credentials.token).exp * 1000; if (expiry <= Date.now()) { - this.update({ credentials: { token: null, email: null } }); + this.update({ credentials: { token: null, email: null, displayName: null } }); } }; diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index d9b6528f8b..704cbbe71d 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -39,12 +39,10 @@ export default class AuthChannel { channel .join() .receive("ok", () => { - channel.on("auth_credentials", async ({ credentials: token, payload: payload }) => { - await this.handleAuthCredentials(payload.email, token); - resolve(); - }); - - channel.push("auth_verified", { token: authToken, payload: authPayload }); + channel.on("auth_credentials", async ({ credentials: token, payload: payload }) => { + await this.handleAuthCredentials({ email: payload.email }, token); + resolve(); + }); }) .receive("error", reject); }); @@ -61,7 +59,7 @@ export default class AuthChannel { const authComplete = new Promise(resolve => channel.on("auth_credentials", async ({ credentials: token }) => { - await this.handleAuthCredentials(email, token, hubChannel); + await this.handleAuthCredentials({ email }, token, hubChannel); resolve(); }) ); @@ -73,8 +71,9 @@ export default class AuthChannel { return { authComplete }; } - async handleAuthCredentials(email, token, hubChannel) { - this.store.update({ credentials: { email, token } }); + async handleAuthCredentials(userInfo, token, hubChannel) { + console.log("handleAuthCredentials", userInfo, token, hubChannel); + this.store.update({ credentials: { ...userInfo, token } }); if (hubChannel) { await hubChannel.signIn(token); diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index ae89e81bf3..bf79a25336 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -189,7 +189,7 @@ export async function createAndRedirectToNewHub(name, sceneId, replace) { if (res.error === "invalid_token") { // Clear the invalid token from store. - store.update({ credentials: { token: null, email: null } }); + store.update({ credentials: { token: null, email: null, displayName: null } }); // Create hub anonymously delete headers.authorization; From 7a0c6a668b97c6c907f8ded93de2bf0f792f9f83 Mon Sep 17 00:00:00 2001 From: netpro2k Date: Thu, 12 Nov 2020 18:07:20 -0800 Subject: [PATCH 02/32] OIDC login --- src/react-components/auth/AuthContext.js | 2 +- src/react-components/auth/VerifyPage.js | 36 ++++++++++++++++---- src/react-components/sign-in-dialog.js | 13 +++++++ src/utils/auth-channel.js | 43 +++++++++++++++++++++++- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/react-components/auth/AuthContext.js b/src/react-components/auth/AuthContext.js index 8f86d73cd3..553b118e56 100644 --- a/src/react-components/auth/AuthContext.js +++ b/src/react-components/auth/AuthContext.js @@ -65,7 +65,7 @@ export function AuthContextProvider({ children, store }) { const authChannel = new AuthChannel(store); const socket = await connectToReticulum(); authChannel.setSocket(socket); - await authChannel.verifyAuthentication(authParams.topic, authParams.token, authParams.payload); + await authChannel.verifyAuthentication(authParams.topic, authParams.token, authParams.payload, authParams.origin); }, [store] ); diff --git a/src/react-components/auth/VerifyPage.js b/src/react-components/auth/VerifyPage.js index 5355235f63..fd409398b4 100644 --- a/src/react-components/auth/VerifyPage.js +++ b/src/react-components/auth/VerifyPage.js @@ -6,6 +6,8 @@ import { Loader } from "../misc/Loader"; import { AuthContext } from "../auth/AuthContext"; import configs from "../../utils/configs"; +import jwtDecode from "jwt-decode"; + const VerificationStep = { verifying: "verifying", complete: "complete", @@ -22,12 +24,28 @@ function useVerify() { try { const qs = new URLSearchParams(location.search); - const authParams = { - topic: qs.get("auth_topic"), - token: qs.get("auth_token"), - origin: qs.get("auth_origin"), - payload: qs.get("auth_payload") - }; + if (qs.get("error")) { + throw new Error(`${qs.get("error")}: ${qs.get("error_description")}`); + } + + let authParams; + if (qs.get("code")) { + const state = qs.get("state"); + const topic_key = jwtDecode(state).topic_key; + authParams = { + topic: `oidc:${topic_key}`, + token: qs.get("code"), + origin: "oidc", + payload: qs.get("state") + }; + } else { + authParams = { + topic: qs.get("auth_topic"), + token: qs.get("auth_token"), + origin: qs.get("auth_origin"), + payload: qs.get("auth_payload") + }; + } await auth.verify(authParams); setStep(VerificationStep.complete); @@ -46,7 +64,7 @@ function useVerify() { function EmailVerifying() { return (
-

Email Verifying

+

Verifying...

); @@ -56,6 +74,10 @@ function EmailVerified() { const qs = new URLSearchParams(location.search); const origin = qs.get("auth_origin"); + useEffect(function() { + window.close(); + }); + return (

Verification Complete

diff --git a/src/react-components/sign-in-dialog.js b/src/react-components/sign-in-dialog.js index e34f130b1e..2c0c5c6b39 100644 --- a/src/react-components/sign-in-dialog.js +++ b/src/react-components/sign-in-dialog.js @@ -33,6 +33,12 @@ export default class SignInDialog extends Component { this.props.onSignIn(this.state.email); }; + startOIDCFlow = e => { + e.preventDefault(); + e.stopPropagation(); + this.props.onSignIn("oidc"); + }; + render() { let contents; if (this.props.authStarted) { @@ -104,6 +110,13 @@ export default class SignInDialog extends Component { ); + + // TODO check app config to decide login type and customize button + contents = ( +
+ +
+ ); } return ( diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index 704cbbe71d..3932fd661b 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -28,7 +28,7 @@ export default class AuthChannel { this._signedIn = false; }; - verifyAuthentication(authTopic, authToken, authPayload) { + verifyAuthentication(authTopic, authToken, authPayload, origin) { const channel = this.socket.channel(authTopic); return new Promise((resolve, reject) => { channel.onError(() => { @@ -39,15 +39,56 @@ export default class AuthChannel { channel .join() .receive("ok", () => { + if (origin === "oidc") { + channel + .push("auth_verified", { token: authToken, payload: authPayload }) + .receive("ok", resolve) + .receive("error", reject); + } else { + channel.push("auth_verified", { token: authToken, payload: authPayload }); channel.on("auth_credentials", async ({ credentials: token, payload: payload }) => { await this.handleAuthCredentials({ email: payload.email }, token); resolve(); }); + } }) .receive("error", reject); }); } + async startOIDCAuthentication(hubChannel) { + const channel = this.socket.channel(`oidc:${uuid()}`); + await new Promise((resolve, reject) => + channel + .join() + .receive("ok", resolve) + .receive("error", reject) + ); + + const authorizeUrl = await new Promise((resolve, reject) => + channel + .push("auth_request") + .receive("ok", function({ authorize_url }) { + resolve(authorize_url); + }) + .receive("error", reject) + ); + + window.open(authorizeUrl, "hubs_oidc"); + + const authComplete = new Promise(resolve => + channel.on("auth_credentials", async ({ user_info, credentials: token }) => { + console.log("got credentials", user_info, token); + await this.handleAuthCredentials(user_info, token, hubChannel); + resolve(); + }) + ); + + // Returning an object with the authComplete promise since we want the caller to wait for the above await but not + // for authComplete. + return { authComplete }; + } + async startAuthentication(email, hubChannel) { const channel = this.socket.channel(`auth:${uuid()}`); await new Promise((resolve, reject) => From 8925f51395dfd5274d870742f72e84d54a033f08 Mon Sep 17 00:00:00 2001 From: netpro2k Date: Tue, 17 Nov 2020 18:18:56 -0800 Subject: [PATCH 03/32] Add admin panel config for OIDC --- admin/src/react-components/service-editor.js | 16 ++++++-- src/assets/locales/en.json | 1 + src/react-components/sign-in-dialog.js | 39 +++++++++++++++----- src/schema.toml | 3 ++ src/utils/configs.js | 7 +++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/admin/src/react-components/service-editor.js b/admin/src/react-components/service-editor.js index 262f11293d..f3f81189a2 100644 --- a/admin/src/react-components/service-editor.js +++ b/admin/src/react-components/service-editor.js @@ -124,6 +124,14 @@ function isEmptyObject(obj) { return true; } +function getConfigurables(categorySchema) { + return getDescriptors(categorySchema).filter( + ([path, descriptor]) => + (qs.get("show_internal_configs") !== null || descriptor.internal !== "true") && + (qs.get("show_oidc_configs") !== null || path[1] !== "oidc") + ); +} + class ConfigurationEditor extends Component { constructor(props) { super(props); @@ -321,9 +329,9 @@ class ConfigurationEditor extends Component { } renderTree(schema, category, config) { - const configurables = getDescriptors(schema[category]) - .filter(([, descriptor]) => qs.get("show_internal_configs") !== null || descriptor.internal !== "true") - .map(([path, descriptor]) => this.renderConfigurable(path, descriptor, getConfigValue(config, path))); + const configurables = getConfigurables(schema[category]).map(([path, descriptor]) => + this.renderConfigurable(path, descriptor, getConfigValue(config, path)) + ); return (
@@ -363,7 +371,7 @@ class ConfigurationEditor extends Component { onChange={this.handleTabChange.bind(this)} > {schemaCategories - .filter(c => this.props.schema[c] && !isEmptyObject(this.props.schema[c])) + .filter(c => this.props.schema[c] && !isEmptyObject(getConfigurables(this.props.schema[c]))) .map(c => ( ))} diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 8207124957..9ec2a0f5b3 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -15,6 +15,7 @@ "sign-in.admin-no-permission": "You don't have access to admin tools. Sign into another account or ask an administrator to grant you permission.", "sign-in.hub": "An account is required to join rooms.\n\nEnter your email to create your account or sign in.", "sign-in.auth-started": "Email sent to {email}!\n\nTo continue, click on the link in the email using your phone, tablet, or PC.\n\nNo email? You may not be able to create an account.", + "sign-in.oidc-auth-started": "Waiting for signin...", "sign-in.pin": "You'll need to sign in to pin objects.", "sign-in.pin-complete": "You are now signed in.", "sign-in.unpin": "You'll need to sign in to un-pin objects.", diff --git a/src/react-components/sign-in-dialog.js b/src/react-components/sign-in-dialog.js index 2c0c5c6b39..bd2b115146 100644 --- a/src/react-components/sign-in-dialog.js +++ b/src/react-components/sign-in-dialog.js @@ -39,10 +39,9 @@ export default class SignInDialog extends Component { this.props.onSignIn("oidc"); }; - render() { - let contents; + renderEmailAuth() { if (this.props.authStarted) { - contents = ( + return (

@@ -58,7 +57,7 @@ export default class SignInDialog extends Component {

); } else if (this.props.authComplete) { - contents = ( + return (

{this.props.message}

); } else { - contents = ( + return ( {this.props.message} ); + } + } - // TODO check app config to decide login type and customize button - contents = ( + renderOIDCAuth() { + if (this.props.authStarted) { + return ( +
+

+ +

+
+ ); + } else if (this.props.authComplete) { + return ( +
+

{this.props.message}

+ +
+ ); + } else { + return (
- +
); } + } + render() { return ( - {contents} + {configs.APP_CONFIG.auth.use_oidc ? this.renderOIDCAuth() : this.renderEmailAuth()} ); } diff --git a/src/schema.toml b/src/schema.toml index 30a809aeb6..702957a174 100644 --- a/src/schema.toml +++ b/src/schema.toml @@ -83,3 +83,6 @@ links.model_collection = { category = "links", type = "string", name = "Model Co auth.login_subject = { category = "auth", type="string", name="Magic Link Email Subject", description="Customize the email subject line for users logging in" } auth.login_body = { category = "auth", type="longstring", name="Magic Link Email Body", description="Customize message. Add '{{ link }}' to insert the magic link, otherwise it will be appended at the end." } + +auth.use_oidc = { category = "auth", type = "boolean", name = "Use OpenID Connect for login", description = "Log in using an OpenID connect provider instead of email verification", internal = "true" } +auth.oidc_button_label = { category = "auth", type="string", name="OpenID Connect Sign In button label", description="Text to display on the sign in button. Ex: Sign in with Google", internal = "true" } diff --git a/src/utils/configs.js b/src/utils/configs.js index 1580554947..ef29fa5784 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -51,9 +51,14 @@ if (window.APP_CONFIG) { if (!configs.APP_CONFIG.features) { configs.APP_CONFIG.features = {}; } + + if (!configs.APP_CONFIG.auth) { + configs.APP_CONFIG.auth = {}; + } } else { configs.APP_CONFIG = { - features: {} + features: {}, + auth: {} }; } From dab2785976560135634bab2d5f439cc1c4e9bbf2 Mon Sep 17 00:00:00 2001 From: netpro2k Date: Tue, 17 Nov 2020 19:18:35 -0800 Subject: [PATCH 04/32] Support for string list properties in admin panel --- admin/src/react-components/service-editor.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/admin/src/react-components/service-editor.js b/admin/src/react-components/service-editor.js index f3f81189a2..7e3dd0e758 100644 --- a/admin/src/react-components/service-editor.js +++ b/admin/src/react-components/service-editor.js @@ -230,6 +230,23 @@ class ConfigurationEditor extends Component { ); } + renderListInput(path, descriptor, currentValue) { + const displayPath = path.join(" > "); + return ( + this.onChange(path, ev.target.value.split(",").map(v => v.trim()))} + helperText={descriptor.description} + type="text" + fullWidth + margin="normal" + /> + ); + } + renderLongTextInput(path, descriptor, currentValue) { const displayPath = path.join(" > "); return ( @@ -312,7 +329,7 @@ class ConfigurationEditor extends Component { renderConfigurable(path, descriptor, currentValue) { switch (descriptor.type) { case "list": - return null; + return descriptor.of === "string" ? this.renderListInput(path, descriptor, currentValue) : null; case "file": return this.renderFileInput(path, descriptor, currentValue); case "boolean": From fff7ca5c37f4c4e90822ae0baac57f746d0dde46 Mon Sep 17 00:00:00 2001 From: netpro2k Date: Wed, 2 Dec 2020 18:06:20 -0800 Subject: [PATCH 05/32] Fix OIDC login for homepage sign in link --- src/react-components/auth/AuthContext.js | 6 ++- src/react-components/auth/SignInPage.js | 51 +++++++++++++++++++++--- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/react-components/auth/AuthContext.js b/src/react-components/auth/AuthContext.js index 553b118e56..8b35221d2a 100644 --- a/src/react-components/auth/AuthContext.js +++ b/src/react-components/auth/AuthContext.js @@ -49,11 +49,13 @@ async function checkIsAdmin(socket, store) { export function AuthContextProvider({ children, store }) { const signIn = useCallback( - async email => { + async authPayload => { const authChannel = new AuthChannel(store); const socket = await connectToReticulum(); authChannel.setSocket(socket); - const { authComplete } = await authChannel.startAuthentication(email); + const { authComplete } = await (authPayload == "oidc" + ? authChannel.startOIDCAuthentication() + : authChannel.startAuthentication(authPayload)); await authComplete; await checkIsAdmin(socket, store); }, diff --git a/src/react-components/auth/SignInPage.js b/src/react-components/auth/SignInPage.js index 5d4032685e..0c40252ad9 100644 --- a/src/react-components/auth/SignInPage.js +++ b/src/react-components/auth/SignInPage.js @@ -15,6 +15,7 @@ const SignInStep = { const SignInAction = { submitEmail: "submitEmail", + submitOIDC: "submitOIDC", verificationReceived: "verificationReceived", cancel: "cancel" }; @@ -28,6 +29,8 @@ function loginReducer(state, action) { switch (action.type) { case SignInAction.submitEmail: return { step: SignInStep.waitForVerification, email: action.email }; + case SignInAction.submitOIDC: + return { step: SignInStep.waitForVerification }; case SignInAction.verificationReceived: return { ...state, step: SignInStep.complete }; case SignInAction.cancel: @@ -49,6 +52,16 @@ function useSignIn() { [auth] ); + const submitOIDC = useCallback( + () => { + auth.signIn("oidc").then(() => { + dispatch({ type: SignInAction.verificationReceived }); + }); + dispatch({ type: SignInAction.submitOIDC }); + }, + [auth] + ); + const cancel = useCallback(() => { dispatch({ type: SignInAction.cancel }); }, []); @@ -57,6 +70,7 @@ function useSignIn() { step: state.step, email: state.email, submitEmail, + submitOIDC, cancel }; } @@ -116,21 +130,42 @@ function SubmitEmail({ onSubmitEmail, initialEmail }) { ); } - SubmitEmail.defaultProps = { initialEmail: "" }; - SubmitEmail.propTypes = { initialEmail: PropTypes.string, onSubmitEmail: PropTypes.func.isRequired }; +function SubmitOIDC({ onSubmitOIDC }) { + const onSubmitForm = useCallback( + e => { + e.preventDefault(); + onSubmitOIDC(); + }, + [onSubmitOIDC] + ); + + return ( +
+ +
+ ); +} +SubmitOIDC.propTypes = { + onSubmitOIDC: PropTypes.func.isRequired +}; + function WaitForVerification({ email, onCancel }) { return (

- + {email ? ( + + ) : ( + + )}

@@ -146,13 +181,13 @@ function WaitForVerification({ email, onCancel }) { } WaitForVerification.propTypes = { - email: PropTypes.string.isRequired, + email: PropTypes.string, onCancel: PropTypes.func.isRequired }; export function SignInPage() { const qs = new URLSearchParams(location.search); - const { step, submitEmail, cancel, email } = useSignIn(); + const { step, submitEmail, submitOIDC, cancel, email } = useSignIn(); const redirectUrl = qs.get("sign_in_destination_url") || "/"; useEffect( @@ -167,7 +202,11 @@ export function SignInPage() { return ( {step === SignInStep.submit ? ( - + configs.APP_CONFIG.auth.use_oidc ? ( + + ) : ( + + ) ) : ( )} From e532bd4fd89430b1a0b85efce7c04641223aa9eb Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 12 Oct 2022 15:26:15 +0100 Subject: [PATCH 06/32] Removed duplicate include --- src/react-components/ui-root.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 6eb525b7b4..d881a9494b 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -8,7 +8,6 @@ import screenfull from "screenfull"; import configs from "../utils/configs"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { canShare } from "../utils/share"; -import maskEmail from "../utils/mask-email"; import styles from "../assets/stylesheets/ui-root.scss"; import styleUtils from "./styles/style-utils.scss"; import { ReactAudioContext } from "./wrap-with-audio"; From 2636f93abb65615df2624d9181547597706682cb Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 12 Oct 2022 15:26:49 +0100 Subject: [PATCH 07/32] Adjusted for SignInPage refactoring --- src/react-components/auth/SignInModal.js | 37 ++- .../auth/SignInModalContainer.js | 41 +++- src/react-components/auth/SignInPage.js | 215 ------------------ 3 files changed, 60 insertions(+), 233 deletions(-) delete mode 100644 src/react-components/auth/SignInPage.js diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index 06d489ea20..0816ca43e3 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -136,15 +136,38 @@ SubmitEmail.propTypes = { onSubmitEmail: PropTypes.func.isRequired }; +function SubmitOIDC({ onSubmitOIDC }) { + const onSubmitForm = useCallback( + e => { + e.preventDefault(); + onSubmitOIDC(); + }, + [onSubmitOIDC] + ); + + return ( +

+ +
+ ); +} +SubmitOIDC.propTypes = { + onSubmitOIDC: PropTypes.func.isRequired +}; + export function WaitForVerification({ email, onCancel, showNewsletterSignup }) { return ( -

{chunks}

}} - /> + {email ? ( +

{chunks}

}} + /> + ) : ( + + )} {showNewsletterSignup && (

@@ -166,7 +189,7 @@ export function WaitForVerification({ email, onCancel, showNewsletterSignup }) { WaitForVerification.propTypes = { showNewsletterSignup: PropTypes.bool, - email: PropTypes.string.isRequired, + email: PropTypes.string, onCancel: PropTypes.func.isRequired }; diff --git a/src/react-components/auth/SignInModalContainer.js b/src/react-components/auth/SignInModalContainer.js index 3f92c08fb5..6d6b001595 100644 --- a/src/react-components/auth/SignInModalContainer.js +++ b/src/react-components/auth/SignInModalContainer.js @@ -2,10 +2,11 @@ import React, { useCallback, useReducer, useContext, useEffect } from "react"; import { TERMS, PRIVACY } from "../../constants"; import configs from "../../utils/configs"; import { AuthContext } from "./AuthContext"; -import { SignInModal, SignInStep, WaitForVerification, SubmitEmail } from "./SignInModal"; +import { SignInModal, SignInStep, WaitForVerification, SubmitEmail, SubmitOIDC } from "./SignInModal"; const SignInAction = { submitEmail: "submitEmail", + submitOIDC: "submitOIDC", verificationReceived: "verificationReceived", cancel: "cancel" }; @@ -19,6 +20,8 @@ function loginReducer(state, action) { switch (action.type) { case SignInAction.submitEmail: return { step: SignInStep.waitForVerification, email: action.email }; + case SignInAction.submitOIDC: + return { step: SignInStep.waitForVerification }; case SignInAction.verificationReceived: return { ...state, step: SignInStep.complete }; case SignInAction.cancel: @@ -40,6 +43,16 @@ function useSignIn() { [auth] ); + const submitOIDC = useCallback( + () => { + auth.signIn("oidc").then(() => { + dispatch({ type: SignInAction.verificationReceived }); + }); + dispatch({ type: SignInAction.submitOIDC }); + }, + [auth] + ); + const cancel = useCallback(() => { dispatch({ type: SignInAction.cancel }); }, []); @@ -48,13 +61,15 @@ function useSignIn() { step: state.step, email: state.email, submitEmail, + submitOIDC, cancel }; } + export function SignInModalContainer() { const qs = new URLSearchParams(location.search); - const { step, submitEmail, cancel, email } = useSignIn(); + const { step, submitEmail, submitOIDC, cancel, email } = useSignIn(); const redirectUrl = qs.get("sign_in_destination_url") || "/"; useEffect( @@ -69,15 +84,19 @@ export function SignInModalContainer() { return ( {step === SignInStep.submit ? ( - + configs.APP_CONFIG.auth.use_oidc ? ( + + ) : ( + + ) ) : ( { - auth.signIn(email).then(() => { - dispatch({ type: SignInAction.verificationReceived }); - }); - dispatch({ type: SignInAction.submitEmail, email }); - }, - [auth] - ); - - const submitOIDC = useCallback( - () => { - auth.signIn("oidc").then(() => { - dispatch({ type: SignInAction.verificationReceived }); - }); - dispatch({ type: SignInAction.submitOIDC }); - }, - [auth] - ); - - const cancel = useCallback(() => { - dispatch({ type: SignInAction.cancel }); - }, []); - - return { - step: state.step, - email: state.email, - submitEmail, - submitOIDC, - cancel - }; -} - -function SubmitEmail({ onSubmitEmail, initialEmail }) { - const [email, setEmail] = useState(initialEmail); - - const onSubmitForm = useCallback( - e => { - e.preventDefault(); - onSubmitEmail(email); - }, - [onSubmitEmail, email] - ); - - return ( -

-

- -

- - - - setEmail(e.target.value)} - placeholder="example@example.com" - /> - {(configs.feature("show_terms") || configs.feature("show_privacy")) && ( - - By proceeding, you agree to the{" "} - -
- terms of use - {" "} - - {configs.feature("show_terms") && configs.feature("show_privacy") && "and "} - - - privacy notice - - . - - )} - - - ); -} -SubmitEmail.defaultProps = { - initialEmail: "" -}; -SubmitEmail.propTypes = { - initialEmail: PropTypes.string, - onSubmitEmail: PropTypes.func.isRequired -}; - -function SubmitOIDC({ onSubmitOIDC }) { - const onSubmitForm = useCallback( - e => { - e.preventDefault(); - onSubmitOIDC(); - }, - [onSubmitOIDC] - ); - - return ( -
- -
- ); -} -SubmitOIDC.propTypes = { - onSubmitOIDC: PropTypes.func.isRequired -}; - -function WaitForVerification({ email, onCancel }) { - return ( -
-

- {email ? ( - - ) : ( - - )} -

- -

- Want Hubs news sent to your inbox?{"\n"} - - Subscribe for updates - . -

-
- -
- ); -} - -WaitForVerification.propTypes = { - email: PropTypes.string, - onCancel: PropTypes.func.isRequired -}; - -export function SignInPage() { - const qs = new URLSearchParams(location.search); - const { step, submitEmail, submitOIDC, cancel, email } = useSignIn(); - const redirectUrl = qs.get("sign_in_destination_url") || "/"; - - useEffect( - () => { - if (step === SignInStep.complete) { - window.location = redirectUrl; - } - }, - [step, redirectUrl] - ); - - return ( - - {step === SignInStep.submit ? ( - configs.APP_CONFIG.auth.use_oidc ? ( - - ) : ( - - ) - ) : ( - - )} - - ); -} From 88416c61ac2bce25064dc262e34e674716949ad6 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 12 Oct 2022 15:35:10 +0100 Subject: [PATCH 08/32] Adjusted to VerifyPage refactoring --- src/react-components/auth/VerifyModal.js | 6 + .../auth/VerifyModalContainer.js | 29 ++++- src/react-components/auth/VerifyPage.js | 118 ------------------ 3 files changed, 29 insertions(+), 124 deletions(-) delete mode 100644 src/react-components/auth/VerifyPage.js diff --git a/src/react-components/auth/VerifyModal.js b/src/react-components/auth/VerifyModal.js index dbc6c7368d..2e567a77dd 100644 --- a/src/react-components/auth/VerifyModal.js +++ b/src/react-components/auth/VerifyModal.js @@ -24,6 +24,12 @@ export function VerifyingEmail() { } export function EmailVerified({ origin }) { + + // FROM ORIGINAL OIDC SPIKE + // useEffect(function() { + // window.close(); + // }); + return ( diff --git a/src/react-components/auth/VerifyModalContainer.js b/src/react-components/auth/VerifyModalContainer.js index c590a082a3..85fcd0873b 100644 --- a/src/react-components/auth/VerifyModalContainer.js +++ b/src/react-components/auth/VerifyModalContainer.js @@ -1,6 +1,7 @@ import React, { useState, useContext, useEffect } from "react"; import { AuthContext } from "./AuthContext"; import { VerifyModal, VerificationError, EmailVerified, VerifyingEmail } from "./VerifyModal"; +import jwtDecode from "jwt-decode"; const VerificationStep = { verifying: "verifying", @@ -19,12 +20,28 @@ function useVerify() { try { const qs = new URLSearchParams(location.search); - const authParams = { - topic: qs.get("auth_topic"), - token: qs.get("auth_token"), - origin: qs.get("auth_origin"), - payload: qs.get("auth_payload") - }; + if (qs.get("error")) { + throw new Error(`${qs.get("error")}: ${qs.get("error_description")}`); + } + + let authParams; + if (qs.get("code")) { + const state = qs.get("state"); + const topic_key = jwtDecode(state).topic_key; + authParams = { + topic: `oidc:${topic_key}`, + token: qs.get("code"), + origin: "oidc", + payload: qs.get("state") + }; + } else { + authParams = { + topic: qs.get("auth_topic"), + token: qs.get("auth_token"), + origin: qs.get("auth_origin"), + payload: qs.get("auth_payload") + }; + } await verify(authParams); setStep(VerificationStep.complete); diff --git a/src/react-components/auth/VerifyPage.js b/src/react-components/auth/VerifyPage.js deleted file mode 100644 index fd409398b4..0000000000 --- a/src/react-components/auth/VerifyPage.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState, useContext, useEffect } from "react"; -import PropTypes from "prop-types"; -import { Page } from "../layout/Page"; -import styles from "./SignInPage.scss"; -import { Loader } from "../misc/Loader"; -import { AuthContext } from "../auth/AuthContext"; -import configs from "../../utils/configs"; - -import jwtDecode from "jwt-decode"; - -const VerificationStep = { - verifying: "verifying", - complete: "complete", - error: "error" -}; - -function useVerify() { - const [step, setStep] = useState(VerificationStep.verifying); - const [error, setError] = useState(); - const auth = useContext(AuthContext); - - useEffect(() => { - const verifyAsync = async () => { - try { - const qs = new URLSearchParams(location.search); - - if (qs.get("error")) { - throw new Error(`${qs.get("error")}: ${qs.get("error_description")}`); - } - - let authParams; - if (qs.get("code")) { - const state = qs.get("state"); - const topic_key = jwtDecode(state).topic_key; - authParams = { - topic: `oidc:${topic_key}`, - token: qs.get("code"), - origin: "oidc", - payload: qs.get("state") - }; - } else { - authParams = { - topic: qs.get("auth_topic"), - token: qs.get("auth_token"), - origin: qs.get("auth_origin"), - payload: qs.get("auth_payload") - }; - } - - await auth.verify(authParams); - setStep(VerificationStep.complete); - } catch (error) { - setStep(VerificationStep.error); - setError(error); - } - }; - - verifyAsync(); - }, []); - - return { step, error }; -} - -function EmailVerifying() { - return ( -
-

Verifying...

- -
- ); -} - -function EmailVerified() { - const qs = new URLSearchParams(location.search); - const origin = qs.get("auth_origin"); - - useEffect(function() { - window.close(); - }); - - return ( -
-

Verification Complete

- Please close this browser window and return to {origin}. -
- ); -} - -function VerificationError({ error }) { - return ( -
-

Error Verifying Email

- {(error && error.message) || "Unknown Error"} -
- ); -} - -VerificationError.propTypes = { - error: PropTypes.object -}; - -export function VerifyPage() { - const { step, error } = useVerify(); - - let content; - - if (step === VerificationStep.error) { - content = ; - } else if (step === VerificationStep.complete) { - content = ; - } else { - content = ; - } - - return ( - {content} - ); -} From a600389a951f024a725a3e9fbc187308094f5ee6 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 12 Oct 2022 15:46:03 +0100 Subject: [PATCH 09/32] Presence list has been refactored into PresenceSidebarContainer --- src/react-components/presence-list.js | 259 -------------------------- 1 file changed, 259 deletions(-) delete mode 100644 src/react-components/presence-list.js diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js deleted file mode 100644 index 260947cddc..0000000000 --- a/src/react-components/presence-list.js +++ /dev/null @@ -1,259 +0,0 @@ -import configs from "../utils/configs"; -import { getMicrophonePresences } from "../utils/microphone-presence"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { FormattedMessage } from "react-intl"; -import classNames from "classnames"; - -import rootStyles from "../assets/stylesheets/ui-root.scss"; -import styles from "../assets/stylesheets/presence-list.scss"; -import StateLink from "./state-link.js"; -import { WithHoverSound } from "./wrap-with-audio"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; -import { faPencilAlt } from "@fortawesome/free-solid-svg-icons/faPencilAlt"; -import { faDesktop } from "@fortawesome/free-solid-svg-icons/faDesktop"; -import { faVideo } from "@fortawesome/free-solid-svg-icons/faVideo"; -import discordIcon from "../assets/images/discord.svgi"; -import hmdIcon from "../assets/images/hmd-icon.svgi"; -import { faMobileAlt } from "@fortawesome/free-solid-svg-icons/faMobileAlt"; -import { pushHistoryPath, withSlug } from "../utils/history"; -import { hasReticulumServer } from "../utils/phoenix-utils"; -import { InlineSVG } from "./svgi"; -import { faVolumeMute } from "@fortawesome/free-solid-svg-icons/faVolumeMute"; -import { faVolumeOff } from "@fortawesome/free-solid-svg-icons/faVolumeOff"; -import { faVolumeUp } from "@fortawesome/free-solid-svg-icons/faVolumeUp"; - -const MIC_PRESENCE_UPDATE_FREQUENCY = 500; - -function getPresenceIcon(ctx) { - if (ctx && ctx.hmd) { - return ; - } else if (ctx && ctx.mobile) { - return ; - } else if (ctx && ctx.discord) { - return ; - } else { - return ; - } -} - -function getMicrophonePresenceIcon(microphonePresence) { - if (microphonePresence.muted) { - return ; - } else if (microphonePresence.talking) { - return ; - } else { - return ; - } -} - -export function navigateToClientInfo(history, clientId) { - const currentParams = new URLSearchParams(history.location.search); - - if (hasReticulumServer() && document.location.host !== configs.RETICULUM_SERVER) { - currentParams.set("client_id", clientId); - pushHistoryPath(history, history.location.pathname, currentParams.toString()); - } else { - pushHistoryPath(history, withSlug(history.location, `/clients/${clientId}`), currentParams.toString()); - } -} - -export default class PresenceList extends Component { - static propTypes = { - hubChannel: PropTypes.object, - presences: PropTypes.object, - history: PropTypes.object, - sessionId: PropTypes.string, - signedIn: PropTypes.bool, - displayName: PropTypes.string, - onSignIn: PropTypes.func, - onSignOut: PropTypes.func, - expanded: PropTypes.bool, - onExpand: PropTypes.func - }; - - updateMicrophoneState = () => { - if (this.props.expanded) { - const microphonePresences = getMicrophonePresences(AFRAME.scenes[0]); - this.setState({ microphonePresences }); - } - this.timeout = setTimeout(this.updateMicrophoneState, MIC_PRESENCE_UPDATE_FREQUENCY); - }; - - navigateToClientInfo = clientId => { - navigateToClientInfo(this.props.history, clientId); - }; - - mute = clientId => { - this.props.hubChannel.mute(clientId); - }; - - muteAll = () => { - const presences = this.props.presences; - for (const [clientId, presence] of Object.entries(presences)) { - if (clientId !== this.props.sessionId) { - const meta = presence.metas[0]; - if (meta.presence === "room" && meta.permissions && !meta.permissions.mute_users) { - this.mute(clientId); - } - } - } - }; - - domForPresence = ([sessionId, data]) => { - const meta = data.metas[data.metas.length - 1]; - const context = meta.context; - const profile = meta.profile; - const recording = meta.streaming || meta.recording; - const icon = recording ? : getPresenceIcon(context); - const isBot = context && context.discord; - const isEntering = context && context.entering; - const isOwner = meta.roles && meta.roles.owner; - const messageId = isEntering ? "presence.entering" : `presence.in_${meta.presence}`; - const badge = isOwner && ( - - ★ - - ); - const microphonePresence = - this.state && this.state.microphonePresences && this.state.microphonePresences.get(sessionId); - const micState = - microphonePresence && meta.presence === "room" ? getMicrophonePresenceIcon(microphonePresence) : ""; - const canMuteUsers = this.props.hubChannel.can("mute_users"); - const isMe = sessionId === this.props.sessionId; - const muted = microphonePresence && microphonePresence.muted; - const canMuteIndividual = canMuteUsers && !isMe && microphonePresence && !microphonePresence.muted; - - return ( - -
-
- {icon} -
-
(canMuteUsers ? this.mute(sessionId) : null)} - > - {micState} -
-
- {sessionId === this.props.sessionId ? ( - - {profile && profile.displayName} - {badge} - - - - - ) : ( -
- {!isBot ? ( - - ) : ( - {profile && profile.displayName} - )} - {badge} -
- )} -
-
- -
-
-
- ); - }; - - componentDidMount() { - this.updateMicrophoneState(); - document.querySelector(".a-canvas").addEventListener( - "mouseup", - () => { - this.props.onExpand(false); - }, - { once: true } - ); - } - - componentWillUnmount() { - clearTimeout(this.timeout); - } - - renderExpandedList() { - const canMuteUsers = this.props.hubChannel.can("mute_users"); - const muteAll = canMuteUsers && ( -
- -
- ); - - return ( -
-
-
- {muteAll} -
- {Object.entries(this.props.presences || {}) - .filter(([k]) => k === this.props.sessionId) - .map(this.domForPresence)} - {Object.entries(this.props.presences || {}) - .filter(([k]) => k !== this.props.sessionId) - .map(this.domForPresence)} -
-
- {this.props.signedIn ? ( -
- - {this.props.displayName} - {" "} - - - -
- ) : ( - - - - )} -
-
-
- ); - } - - render() { - const occupantCount = this.props.presences ? Object.entries(this.props.presences).length : 0; - return ( -
- - {this.props.expanded && this.renderExpandedList()} -
- ); - } -} From f2ca22ca2adf9a62bd2c1f62c72e4ff2699dfb33 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 13 Oct 2022 09:49:45 +0100 Subject: [PATCH 10/32] Fixed deprecated flag logic and removed 'show_oidc_configs' as unnecessary while feature is behind 'show_internal_configs' anyway --- admin/src/react-components/service-editor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/admin/src/react-components/service-editor.js b/admin/src/react-components/service-editor.js index 00eed6479c..204d335ce0 100644 --- a/admin/src/react-components/service-editor.js +++ b/admin/src/react-components/service-editor.js @@ -128,8 +128,7 @@ function getConfigurables(categorySchema) { return getDescriptors(categorySchema).filter( ([path, descriptor]) => (qs.get("show_internal_configs") !== null || descriptor.internal !== "true") && - (qs.get("show_deprecated_configs") !== null || descriptor.internal !== "true") && - (qs.get("show_oidc_configs") !== null || path[1] !== "oidc") + (qs.get("show_deprecated_configs") !== null || descriptor.deprecated !== "true") ); } From 1c3fe67c6a3152057ba7adb673eeaa3383f6f637 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 13 Oct 2022 09:50:23 +0100 Subject: [PATCH 11/32] Added missing include in Header --- src/react-components/layout/Header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/layout/Header.js b/src/react-components/layout/Header.js index b4c7cd5cb9..ac5e57bc9c 100644 --- a/src/react-components/layout/Header.js +++ b/src/react-components/layout/Header.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import PropTypes from "prop-types"; import { FormattedMessage } from "react-intl"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; From 0611b1038554d3e84e562cf0c6bd54761cf41a39 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 13 Oct 2022 09:51:36 +0100 Subject: [PATCH 12/32] Added missing export directive --- src/react-components/auth/SignInModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index 0816ca43e3..d57f9329a7 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -136,7 +136,7 @@ SubmitEmail.propTypes = { onSubmitEmail: PropTypes.func.isRequired }; -function SubmitOIDC({ onSubmitOIDC }) { +export function SubmitOIDC({ onSubmitOIDC }) { const onSubmitForm = useCallback( e => { e.preventDefault(); From d3aa9122cc8c7899b0deeea97d05d56ebbabe4ae Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 11:05:31 +0100 Subject: [PATCH 13/32] Verification is not longer just for emails --- src/verify.html | 2 +- src/verify.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verify.html b/src/verify.html index 8ee0ea6e63..76d87b6e09 100644 --- a/src/verify.html +++ b/src/verify.html @@ -8,7 +8,7 @@ - Verify Email + Verify Account diff --git a/src/verify.js b/src/verify.js index 85457a2664..313e847553 100644 --- a/src/verify.js +++ b/src/verify.js @@ -12,7 +12,7 @@ import { PageContainer } from "./react-components/layout/PageContainer"; import { Center } from "./react-components/layout/Center"; import { ThemeProvider } from "./react-components/styles/theme"; -registerTelemetry("/verify", "Hubs Verify Email Page"); +registerTelemetry("/verify", "Hubs Verify Account Page"); const store = new Store(); window.APP = { store }; From d7dca258711524a4b8a6983d3982b785a39978ac Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 11:08:12 +0100 Subject: [PATCH 14/32] OIDC support for modal sign-in dialog (from room context) --- .../auth/RoomSignInModalContainer.js | 39 +++++++++++-------- .../auth/SignInModalContainer.js | 4 +- src/react-components/ui-root.js | 29 +++----------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/react-components/auth/RoomSignInModalContainer.js b/src/react-components/auth/RoomSignInModalContainer.js index d067f17237..38ac2b30a3 100644 --- a/src/react-components/auth/RoomSignInModalContainer.js +++ b/src/react-components/auth/RoomSignInModalContainer.js @@ -1,28 +1,35 @@ -import React, { useState } from "react"; +import React, { useState, useContext } from "react"; import PropTypes from "prop-types"; import configs from "../../utils/configs"; -import { SignInModal, SignInStep, SubmitEmail, WaitForVerification, SignInComplete } from "./SignInModal"; +import { SignInModal, SignInStep, SubmitEmail, SubmitOIDC, WaitForVerification, SignInComplete } from "./SignInModal"; import { TERMS, PRIVACY } from "../../constants"; // TODO: Migrate to use AuthContext -export function RoomSignInModalContainer({ onClose, step, onSubmitEmail, message, onContinue }) { +export function RoomSignInModalContainer({ onClose, step, onSignIn, message, onContinue }) { const [cachedEmail, setCachedEmail] = useState(); + // Can't use `useContext(AuthContext)` unless component is wrapped in an AuthContext element + const auth = configs.APP_CONFIG.auth; + return ( {step === SignInStep.submit && ( - { - setCachedEmail(email); - onSubmitEmail(email); - }} - initialEmail={cachedEmail} - termsUrl={configs.link("terms_of_use", TERMS)} - showTerms={configs.feature("show_terms")} - privacyUrl={configs.link("privacy_notice", PRIVACY)} - showPrivacy={configs.feature("show_privacy")} - message={message} - /> + auth.use_oidc + ? onSignIn("oidc")} + /> + : { + setCachedEmail(email); + onSignIn(email); + }} + initialEmail={cachedEmail} + termsUrl={configs.link("terms_of_use", TERMS)} + showTerms={configs.feature("show_terms")} + privacyUrl={configs.link("privacy_notice", PRIVACY)} + showPrivacy={configs.feature("show_privacy")} + message={message} + /> )} {step === SignInStep.waitForVerification && ( { if (step === SignInStep.complete) { @@ -84,7 +86,7 @@ export function SignInModalContainer() { return ( {step === SignInStep.submit ? ( - configs.APP_CONFIG.auth.use_oidc ? ( + auth.use_oidc ? ( ) : ( { - const { authComplete } = await authChannel.startAuthentication(email, this.props.hubChannel); + onSignIn: async authPayload => { + const { authComplete } = await (authPayload == "oidc" + ? authChannel.startOIDCAuthentication(this.props.hubChannel) + : authChannel.startAuthentication(authPayload, this.props.hubChannel)); this.showNonHistoriedDialog(RoomSignInModalContainer, { step: SignInStep.waitForVerification, @@ -672,25 +674,6 @@ class UIRoot extends Component { renderDialog = (DialogClass, props = {}) => ; - // This logic may need to be somewhere else now - // showSignInDialog = () => { - // this.showNonHistoriedDialog(SignInDialog, { - // message: getMessages()["sign-in.prompt"], - // onSignIn: async authPayload => { - // const { authComplete } = await (authPayload == "oidc" - // ? this.props.authChannel.startOIDCAuthentication(this.props.hubChannel) - // : this.props.authChannel.startAuthentication(authPayload, this.props.hubChannel)); - - // this.showNonHistoriedDialog(SignInDialog, { authStarted: true }); - - // await authComplete; - - // this.setState({ signedIn: true }); - // this.closeDialog(); - // } - // }); - // }; - signOut = async () => { await this.props.authChannel.signOut(this.props.hubChannel); this.setState({ signedIn: false }); @@ -1111,8 +1094,8 @@ class UIRoot extends Component { ) : ( ), items: [ From 129f38968ecd6f84657b17b6483677f2158e6c71 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 11:09:58 +0100 Subject: [PATCH 15/32] Removed deprecate file --- src/react-components/sign-in-dialog.js | 149 ------------------------- 1 file changed, 149 deletions(-) delete mode 100644 src/react-components/sign-in-dialog.js diff --git a/src/react-components/sign-in-dialog.js b/src/react-components/sign-in-dialog.js deleted file mode 100644 index c9da758182..0000000000 --- a/src/react-components/sign-in-dialog.js +++ /dev/null @@ -1,149 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { FormattedMessage } from "react-intl"; - -import configs from "../utils/configs"; -import IfFeature from "./if-feature"; -import styles from "../assets/stylesheets/sign-in-dialog.scss"; -import DialogContainer from "./dialog-container"; -import { handleTextFieldFocus, handleTextFieldBlur } from "../utils/focus-utils"; - -export default class SignInDialog extends Component { - static propTypes = { - authStarted: PropTypes.bool, - authComplete: PropTypes.bool, - onSignIn: PropTypes.func, - onContinue: PropTypes.func, - message: PropTypes.string, - continueText: PropTypes.string, - closable: PropTypes.bool - }; - - static defaultProps = { - closable: true - }; - - state = { - email: "" - }; - - onSubmit = e => { - e.preventDefault(); - e.stopPropagation(); - this.props.onSignIn(this.state.email); - }; - - startOIDCFlow = e => { - e.preventDefault(); - e.stopPropagation(); - this.props.onSignIn("oidc"); - }; - - renderEmailAuth() { - if (this.props.authStarted) { - return ( -
-

- -

- -

- Want Hubs news sent to your inbox?{"\n"} - - Subscribe for updates - . -

-
-
- ); - } else if (this.props.authComplete) { - return ( -
-

{this.props.message}

- -
- ); - } else { - return ( -
- {this.props.message} - handleTextFieldFocus(e.target)} - onBlur={() => handleTextFieldBlur()} - onChange={e => this.setState({ email: e.target.value })} - className={styles.emailField} - /> - {(configs.feature("show_terms") || configs.feature("show_privacy")) && ( -

- By proceeding, you agree to the{" "} - - - terms of use - {" "} - - {configs.feature("show_terms") && configs.feature("show_privacy") && "and "} - - - privacy notice - - . -

- )} - -
- ); - } - } - - renderOIDCAuth() { - if (this.props.authStarted) { - return ( -
-

- -

-
- ); - } else if (this.props.authComplete) { - return ( -
-

{this.props.message}

- -
- ); - } else { - return ( -
- -
- ); - } - } - - render() { - return ( - - {configs.APP_CONFIG.auth.use_oidc ? this.renderOIDCAuth() : this.renderEmailAuth()} - - ); - } -} From 40a52e8e05e3cd606e872f1c06511c6cdc96ab2a Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 12:11:37 +0100 Subject: [PATCH 16/32] Match OIDC dialog to email dialog --- .../auth/RoomSignInModalContainer.js | 13 +++++++-- src/react-components/auth/SignInModal.js | 28 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/react-components/auth/RoomSignInModalContainer.js b/src/react-components/auth/RoomSignInModalContainer.js index 38ac2b30a3..8f2a6e7adb 100644 --- a/src/react-components/auth/RoomSignInModalContainer.js +++ b/src/react-components/auth/RoomSignInModalContainer.js @@ -14,11 +14,17 @@ export function RoomSignInModalContainer({ onClose, step, onSignIn, message, onC return ( {step === SignInStep.submit && ( - auth.use_oidc - ? onSignIn("oidc")} + termsUrl={configs.link("terms_of_use", TERMS)} + showTerms={configs.feature("show_terms")} + privacyUrl={configs.link("privacy_notice", PRIVACY)} + showPrivacy={configs.feature("show_privacy")} + message={message} /> - : { setCachedEmail(email); onSignIn(email); @@ -30,6 +36,7 @@ export function RoomSignInModalContainer({ onClose, step, onSignIn, message, onC showPrivacy={configs.feature("show_privacy")} message={message} /> + ) )} {step === SignInStep.waitForVerification && ( { e.preventDefault(); @@ -146,13 +147,28 @@ export function SubmitOIDC({ onSubmitOIDC }) { ); return ( -
- -
+ +

+ {message ? ( + intl.formatMessage(message) + ) : ( + + )} +

+

+ + + +

+ +
); } SubmitOIDC.propTypes = { - onSubmitOIDC: PropTypes.func.isRequired + onSubmitOIDC: PropTypes.func.isRequired, + message: PropTypes.object, + termsUrl: PropTypes.string, + privacyUrl: PropTypes.string }; export function WaitForVerification({ email, onCancel, showNewsletterSignup }) { From 59371c07d5627eaf641c287f80fe82a319899a1c Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 14:43:42 +0100 Subject: [PATCH 17/32] Added "extras" to credentials for handling OIDC specific fields Simplified original displayName passthrough --- src/react-components/auth/AuthContext.js | 8 +++----- src/react-components/ui-root.js | 26 +----------------------- src/storage/store.js | 4 ++-- src/utils/auth-channel.js | 17 ++++++++-------- src/utils/mask-email.js | 6 +++++- src/utils/phoenix-utils.js | 2 +- 6 files changed, 21 insertions(+), 42 deletions(-) diff --git a/src/react-components/auth/AuthContext.js b/src/react-components/auth/AuthContext.js index c19f030f2b..a7f1b7490e 100644 --- a/src/react-components/auth/AuthContext.js +++ b/src/react-components/auth/AuthContext.js @@ -97,7 +97,7 @@ export function AuthContextProvider({ children, store }) { const signOut = useCallback( async () => { configs.setIsAdmin(false); - store.update({ credentials: { token: null, email: null } }); + store.update({ credentials: { token: null, email: null, extras: null } }); await store.resetToRandomDefaultAvatar(); }, [store] @@ -107,8 +107,7 @@ export function AuthContextProvider({ children, store }) { initialized: false, isSignedIn: !!store.state.credentials && !!store.state.credentials.token, isAdmin: configs.isAdmin(), - displayName: - store.state.credentials && (store.state.credentials.displayName || maskEmail(store.state.credentials.email)), + displayName: store.state.credentials && maskEmail(store.state.credentials.email), userId: store.credentialsAccountId, signIn, verify, @@ -124,8 +123,7 @@ export function AuthContextProvider({ children, store }) { isSignedIn: !!store.state.credentials && !!store.state.credentials.token, isAdmin: configs.isAdmin(), displayName: - store.state.credentials && - (store.state.credentials.displayName || maskEmail(store.state.credentials.email)), + store.state.credentials && maskEmail(store.state.credentials.email), userId: store.credentialsAccountId })); }; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 64db07a351..6b1b131319 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1095,7 +1095,7 @@ class UIRoot extends Component { ), items: [ @@ -1346,30 +1346,6 @@ class UIRoot extends Component { /> )} -{/* This functionality belongs in here somewhere */} - {/* {showPresenceList && ( - { - if (expand) { - this.setState({ isPresenceListExpanded: expand, isObjectListExpanded: false }); - } else { - this.setState({ isPresenceListExpanded: expand }); - } - }} - /> - )} */} - {this.props.hub && ( channel.on("auth_credentials", async ({ user_info, credentials: token }) => { - console.log("got credentials", user_info, token); - await this.handleAuthCredentials(user_info, token, hubChannel); + const oidc = user_info.oidc; + // Favour email to match more closely to default Hubs operation + const email = oidc.email || oidc.preferred_username || oidc.name || oidc.sub; + // Include OIDC user info for potential use in custom clients + await this.handleAuthCredentials(email, token, hubChannel, oidc); resolve(); }) ); @@ -100,7 +103,7 @@ export default class AuthChannel { const authComplete = new Promise(resolve => channel.on("auth_credentials", async ({ credentials: token }) => { - await this.handleAuthCredentials({ email }, token, hubChannel); + await this.handleAuthCredentials(email, token, hubChannel); resolve(); }) ); @@ -112,10 +115,8 @@ export default class AuthChannel { return { authComplete }; } - async handleAuthCredentials(userInfo, token, hubChannel) { - console.log("handleAuthCredentials", userInfo, token, hubChannel); - this.store.update({ credentials: { ...userInfo, token } }); - + async handleAuthCredentials(email, token, hubChannel, extras) { + this.store.update({ credentials: { email: email, token: token, extras: extras } }); if (hubChannel) { await hubChannel.signIn(token); } diff --git a/src/utils/mask-email.js b/src/utils/mask-email.js index 489a085f77..20caabd3de 100644 --- a/src/utils/mask-email.js +++ b/src/utils/mask-email.js @@ -4,5 +4,9 @@ export default function maskEmail(email) { const emailIdentity = emailParts[0]; const emailDomain = emailParts[1]; const truncatedIdentity = emailIdentity.substring(0, Math.min(emailIdentity.length, 3)); - return `${truncatedIdentity}...@${emailDomain}`; + if(emailDomain) { + return `${truncatedIdentity}...@${emailDomain}` + } else { + return `${truncatedIdentity}...` + } } diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index ce25728c0d..ac5b6f327a 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -218,7 +218,7 @@ export async function createAndRedirectToNewHub(name, sceneId, replace) { if (res.error === "invalid_token") { // Clear the invalid token from store. - store.update({ credentials: { token: null, email: null, displayName: null } }); + store.update({ credentials: { token: null, email: null, extras: null } }); // Create hub anonymously delete headers.authorization; From 045545371d7d3e69ae37f7996a41ea606b948a79 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 17 Oct 2022 15:22:24 +0100 Subject: [PATCH 18/32] Simplifications to improve mergability --- src/react-components/auth/AuthContext.js | 6 ++---- src/react-components/auth/VerifyModal.js | 6 ------ src/react-components/layout/Header.js | 7 +++---- src/react-components/ui-root.js | 5 ++--- src/utils/auth-channel.js | 2 +- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/react-components/auth/AuthContext.js b/src/react-components/auth/AuthContext.js index a7f1b7490e..821ef7bd88 100644 --- a/src/react-components/auth/AuthContext.js +++ b/src/react-components/auth/AuthContext.js @@ -1,7 +1,6 @@ import React, { createContext, useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import configs from "../../utils/configs"; -import maskEmail from "../../utils/mask-email"; // TODO: We really shouldn't include these dependencies on every page. A dynamic import would work better. import jwtDecode from "jwt-decode"; @@ -107,7 +106,7 @@ export function AuthContextProvider({ children, store }) { initialized: false, isSignedIn: !!store.state.credentials && !!store.state.credentials.token, isAdmin: configs.isAdmin(), - displayName: store.state.credentials && maskEmail(store.state.credentials.email), + email: store.state.credentials && store.state.credentials.email, userId: store.credentialsAccountId, signIn, verify, @@ -122,8 +121,7 @@ export function AuthContextProvider({ children, store }) { ...state, isSignedIn: !!store.state.credentials && !!store.state.credentials.token, isAdmin: configs.isAdmin(), - displayName: - store.state.credentials && maskEmail(store.state.credentials.email), + email: store.state.credentials && store.state.credentials.email, userId: store.credentialsAccountId })); }; diff --git a/src/react-components/auth/VerifyModal.js b/src/react-components/auth/VerifyModal.js index 2e567a77dd..dbc6c7368d 100644 --- a/src/react-components/auth/VerifyModal.js +++ b/src/react-components/auth/VerifyModal.js @@ -24,12 +24,6 @@ export function VerifyingEmail() { } export function EmailVerified({ origin }) { - - // FROM ORIGINAL OIDC SPIKE - // useEffect(function() { - // window.close(); - // }); - return ( diff --git a/src/react-components/layout/Header.js b/src/react-components/layout/Header.js index ac5e57bc9c..aa0dfbed97 100644 --- a/src/react-components/layout/Header.js +++ b/src/react-components/layout/Header.js @@ -1,14 +1,14 @@ -import React, { useContext } from "react"; +import React from "react"; import PropTypes from "prop-types"; import { FormattedMessage } from "react-intl"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; +import maskEmail from "../../utils/mask-email"; import styles from "./Header.scss"; import { Container } from "./Container"; import { SocialBar } from "../home/SocialBar"; import { SignInButton } from "../home/SignInButton"; import { AppLogo } from "../misc/AppLogo"; -import { AuthContext } from "../auth/AuthContext"; export function Header({ showCloud, @@ -25,7 +25,6 @@ export function Header({ onSignOut, isHmc }) { - const auth = useContext(AuthContext); return (
@@ -103,7 +102,7 @@ export function Header({ diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 6b1b131319..75c4d0c05d 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1094,8 +1094,8 @@ class UIRoot extends Component { ) : ( ), items: [ @@ -1345,7 +1345,6 @@ class UIRoot extends Component { scene={this.props.scene} /> )} - {this.props.hub && ( { await this.handleAuthCredentials({ email: payload.email }, token); resolve(); }); + channel.push("auth_verified", { token: authToken, payload: authPayload }); } }) .receive("error", reject); From c07d831df57b1aed01dfef0f1de62a700284605b Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 19 Oct 2022 13:08:39 +0100 Subject: [PATCH 19/32] Truncate user ID differently when not an email --- src/utils/mask-email.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/mask-email.js b/src/utils/mask-email.js index 20caabd3de..ee906ef739 100644 --- a/src/utils/mask-email.js +++ b/src/utils/mask-email.js @@ -3,10 +3,9 @@ export default function maskEmail(email) { const emailParts = email.split("@"); const emailIdentity = emailParts[0]; const emailDomain = emailParts[1]; - const truncatedIdentity = emailIdentity.substring(0, Math.min(emailIdentity.length, 3)); if(emailDomain) { - return `${truncatedIdentity}...@${emailDomain}` + return `${emailIdentity.substring(0, Math.min(emailIdentity.length, 3))}...@${emailDomain}` } else { - return `${truncatedIdentity}...` + return emailIdentity.length > 15 ? `${emailIdentity.substring(0, 15)}...` : emailIdentity; } } From 5ac5835287d77da786698f9ccd8db0ff96b43341 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 19 Oct 2022 16:17:45 +0100 Subject: [PATCH 20/32] Move from "Email" to "Account" in Verify UI --- src/react-components/auth/VerifyModal.js | 8 ++++---- src/react-components/auth/VerifyModal.stories.js | 6 +++--- src/react-components/auth/VerifyModalContainer.js | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/react-components/auth/VerifyModal.js b/src/react-components/auth/VerifyModal.js index dbc6c7368d..e0a2db9339 100644 --- a/src/react-components/auth/VerifyModal.js +++ b/src/react-components/auth/VerifyModal.js @@ -11,11 +11,11 @@ export const VerificationStep = { error: "error" }; -export function VerifyingEmail() { +export function VerifyingAccount() { return ( - +
@@ -23,7 +23,7 @@ export function VerifyingEmail() { ); } -export function EmailVerified({ origin }) { +export function AccountVerified({ origin }) { return ( @@ -40,7 +40,7 @@ export function EmailVerified({ origin }) { ); } -EmailVerified.propTypes = { +AccountVerified.propTypes = { origin: PropTypes.string.isRequired }; diff --git a/src/react-components/auth/VerifyModal.stories.js b/src/react-components/auth/VerifyModal.stories.js index e798207688..b01d34ae52 100644 --- a/src/react-components/auth/VerifyModal.stories.js +++ b/src/react-components/auth/VerifyModal.stories.js @@ -1,7 +1,7 @@ import React from "react"; import { Center } from "../layout/Center"; import { Page } from "../layout/Page"; -import { VerifyModal, VerifyingEmail, EmailVerified, VerificationError } from "./VerifyModal"; +import { VerifyModal, VerifyingAccount, AccountVerified, VerificationError } from "./VerifyModal"; import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png"; export default { @@ -15,7 +15,7 @@ export const Verifying = () => (
- +
@@ -25,7 +25,7 @@ export const Verified = () => (
- +
diff --git a/src/react-components/auth/VerifyModalContainer.js b/src/react-components/auth/VerifyModalContainer.js index 85fcd0873b..20b9c352f5 100644 --- a/src/react-components/auth/VerifyModalContainer.js +++ b/src/react-components/auth/VerifyModalContainer.js @@ -1,6 +1,6 @@ import React, { useState, useContext, useEffect } from "react"; import { AuthContext } from "./AuthContext"; -import { VerifyModal, VerificationError, EmailVerified, VerifyingEmail } from "./VerifyModal"; +import { VerifyModal, VerificationError, AccountVerified, VerifyingAccount } from "./VerifyModal"; import jwtDecode from "jwt-decode"; const VerificationStep = { @@ -69,9 +69,9 @@ export function VerifyModalContainer() { } else if (step === VerificationStep.complete) { const qs = new URLSearchParams(location.search); const origin = qs.get("auth_origin"); - content = ; + content = ; } else { - content = ; + content = ; } return {content}; From ce6c71d7f5346958ea6467c0657a1fee2e9c2368 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Wed, 19 Oct 2022 16:18:26 +0100 Subject: [PATCH 21/32] Use "sub" exclusively on client-side so there is consistency with server view of account --- src/utils/auth-channel.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index 7239f8ce59..9e9009066f 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -78,11 +78,8 @@ export default class AuthChannel { const authComplete = new Promise(resolve => channel.on("auth_credentials", async ({ user_info, credentials: token }) => { - const oidc = user_info.oidc; - // Favour email to match more closely to default Hubs operation - const email = oidc.email || oidc.preferred_username || oidc.name || oidc.sub; // Include OIDC user info for potential use in custom clients - await this.handleAuthCredentials(email, token, hubChannel, oidc); + await this.handleAuthCredentials(user_info.oidc.sub, token, hubChannel, user_info.oidc); resolve(); }) ); From 4c2c5bd059e328715bf94acbd858257597638ab4 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 20 Oct 2022 07:59:09 +0100 Subject: [PATCH 22/32] Show email login in OIDC mode with URL parameter --- .../auth/RoomSignInModalContainer.js | 5 +-- .../auth/SignInModalContainer.js | 38 +++++++++++-------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/react-components/auth/RoomSignInModalContainer.js b/src/react-components/auth/RoomSignInModalContainer.js index 8f2a6e7adb..5a81e973bc 100644 --- a/src/react-components/auth/RoomSignInModalContainer.js +++ b/src/react-components/auth/RoomSignInModalContainer.js @@ -8,13 +8,10 @@ import { TERMS, PRIVACY } from "../../constants"; export function RoomSignInModalContainer({ onClose, step, onSignIn, message, onContinue }) { const [cachedEmail, setCachedEmail] = useState(); - // Can't use `useContext(AuthContext)` unless component is wrapped in an AuthContext element - const auth = configs.APP_CONFIG.auth; - return ( {step === SignInStep.submit && ( - auth.use_oidc ? ( + configs.APP_CONFIG.auth.use_oidc ? ( onSignIn("oidc")} termsUrl={configs.link("terms_of_use", TERMS)} diff --git a/src/react-components/auth/SignInModalContainer.js b/src/react-components/auth/SignInModalContainer.js index 84944cf465..33d4f1732b 100644 --- a/src/react-components/auth/SignInModalContainer.js +++ b/src/react-components/auth/SignInModalContainer.js @@ -3,6 +3,7 @@ import { TERMS, PRIVACY } from "../../constants"; import configs from "../../utils/configs"; import { AuthContext } from "./AuthContext"; import { SignInModal, SignInStep, WaitForVerification, SubmitEmail, SubmitOIDC } from "./SignInModal"; +import { Column } from "../layout/Column"; const SignInAction = { submitEmail: "submitEmail", @@ -72,8 +73,6 @@ export function SignInModalContainer() { const { step, submitEmail, submitOIDC, cancel, email } = useSignIn(); const redirectUrl = qs.get("sign_in_destination_url") || "/"; - const auth = useContext(AuthContext); - useEffect( () => { if (step === SignInStep.complete) { @@ -82,23 +81,30 @@ export function SignInModalContainer() { }, [step, redirectUrl] ); - return ( {step === SignInStep.submit ? ( - auth.use_oidc ? ( - - ) : ( - - ) + + {configs.APP_CONFIG.auth.use_oidc && + + } + {(!configs.APP_CONFIG.auth.use_oidc || qs.has("show_email_signin")) && ( + + )} + ) : ( Date: Thu, 20 Oct 2022 08:38:08 +0100 Subject: [PATCH 23/32] Rationalise function parameters --- src/utils/auth-channel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index 9e9009066f..3423d4df49 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -46,7 +46,7 @@ export default class AuthChannel { .receive("error", reject); } else { channel.on("auth_credentials", async ({ credentials: token, payload: payload }) => { - await this.handleAuthCredentials({ email: payload.email }, token); + await this.handleAuthCredentials(payload.email, token); resolve(); }); channel.push("auth_verified", { token: authToken, payload: authPayload }); @@ -78,7 +78,7 @@ export default class AuthChannel { const authComplete = new Promise(resolve => channel.on("auth_credentials", async ({ user_info, credentials: token }) => { - // Include OIDC user info for potential use in custom clients + // `sub` is the definative user identifier, even though it may not be an email await this.handleAuthCredentials(user_info.oidc.sub, token, hubChannel, user_info.oidc); resolve(); }) @@ -113,7 +113,7 @@ export default class AuthChannel { } async handleAuthCredentials(email, token, hubChannel, extras) { - this.store.update({ credentials: { email: email, token: token, extras: extras } }); + this.store.update({ credentials: { email, token, extras } }); if (hubChannel) { await hubChannel.signIn(token); } From 114d4cdf323e0aab9b5d1beddee03e659235f22b Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 20 Oct 2022 08:51:29 +0100 Subject: [PATCH 24/32] Origin parameter is mandatory, so provide a suitable default --- src/react-components/auth/VerifyModalContainer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-components/auth/VerifyModalContainer.js b/src/react-components/auth/VerifyModalContainer.js index 20b9c352f5..9b81f1c192 100644 --- a/src/react-components/auth/VerifyModalContainer.js +++ b/src/react-components/auth/VerifyModalContainer.js @@ -68,7 +68,8 @@ export function VerifyModalContainer() { content = ; } else if (step === VerificationStep.complete) { const qs = new URLSearchParams(location.search); - const origin = qs.get("auth_origin"); + // auth_origin is not set when using OIDC flow so use a neutral default + const origin = qs.get("auth_origin") || "Hubs"; content = ; } else { content = ; From f2ca29a640b215e56b608d28c6f6de3853f13c98 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Thu, 20 Oct 2022 09:28:06 +0100 Subject: [PATCH 25/32] Formatting for wait dialog --- src/react-components/auth/SignInModal.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index 9dc985542d..1c154b378b 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -182,7 +182,9 @@ export function WaitForVerification({ email, onCancel, showNewsletterSignup }) { values={{ email, p: chunks =>

{chunks}

}} /> ) : ( - +

+ +

)} {showNewsletterSignup && (

From cd48097a78125f6a75e5dba5231254ba580635ba Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Fri, 21 Oct 2022 11:42:00 +0100 Subject: [PATCH 26/32] Removed stray nop changes --- src/react-components/auth/RoomSignInModalContainer.js | 2 +- src/react-components/auth/SignInModalContainer.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-components/auth/RoomSignInModalContainer.js b/src/react-components/auth/RoomSignInModalContainer.js index 5a81e973bc..961e845c42 100644 --- a/src/react-components/auth/RoomSignInModalContainer.js +++ b/src/react-components/auth/RoomSignInModalContainer.js @@ -1,4 +1,4 @@ -import React, { useState, useContext } from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import configs from "../../utils/configs"; import { SignInModal, SignInStep, SubmitEmail, SubmitOIDC, WaitForVerification, SignInComplete } from "./SignInModal"; diff --git a/src/react-components/auth/SignInModalContainer.js b/src/react-components/auth/SignInModalContainer.js index 33d4f1732b..6c470a7de6 100644 --- a/src/react-components/auth/SignInModalContainer.js +++ b/src/react-components/auth/SignInModalContainer.js @@ -81,6 +81,7 @@ export function SignInModalContainer() { }, [step, redirectUrl] ); + return ( {step === SignInStep.submit ? ( From 6946ebdf357554ff3ad83fd7cb1283dcb61d0d19 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 24 Oct 2022 08:30:10 +0100 Subject: [PATCH 27/32] Revert verification page name change to simplify PR --- src/react-components/auth/VerifyModal.js | 8 ++++---- src/react-components/auth/VerifyModal.stories.js | 6 +++--- src/react-components/auth/VerifyModalContainer.js | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/react-components/auth/VerifyModal.js b/src/react-components/auth/VerifyModal.js index e0a2db9339..dbc6c7368d 100644 --- a/src/react-components/auth/VerifyModal.js +++ b/src/react-components/auth/VerifyModal.js @@ -11,11 +11,11 @@ export const VerificationStep = { error: "error" }; -export function VerifyingAccount() { +export function VerifyingEmail() { return ( - +
@@ -23,7 +23,7 @@ export function VerifyingAccount() { ); } -export function AccountVerified({ origin }) { +export function EmailVerified({ origin }) { return ( @@ -40,7 +40,7 @@ export function AccountVerified({ origin }) { ); } -AccountVerified.propTypes = { +EmailVerified.propTypes = { origin: PropTypes.string.isRequired }; diff --git a/src/react-components/auth/VerifyModal.stories.js b/src/react-components/auth/VerifyModal.stories.js index b01d34ae52..e798207688 100644 --- a/src/react-components/auth/VerifyModal.stories.js +++ b/src/react-components/auth/VerifyModal.stories.js @@ -1,7 +1,7 @@ import React from "react"; import { Center } from "../layout/Center"; import { Page } from "../layout/Page"; -import { VerifyModal, VerifyingAccount, AccountVerified, VerificationError } from "./VerifyModal"; +import { VerifyModal, VerifyingEmail, EmailVerified, VerificationError } from "./VerifyModal"; import backgroundUrl from "../../assets/images/home-hero-background-unbranded.png"; export default { @@ -15,7 +15,7 @@ export const Verifying = () => (

- +
@@ -25,7 +25,7 @@ export const Verified = () => (
- +
diff --git a/src/react-components/auth/VerifyModalContainer.js b/src/react-components/auth/VerifyModalContainer.js index 9b81f1c192..f8241d1b08 100644 --- a/src/react-components/auth/VerifyModalContainer.js +++ b/src/react-components/auth/VerifyModalContainer.js @@ -1,6 +1,6 @@ import React, { useState, useContext, useEffect } from "react"; import { AuthContext } from "./AuthContext"; -import { VerifyModal, VerificationError, AccountVerified, VerifyingAccount } from "./VerifyModal"; +import { VerifyModal, VerificationError, EmailVerified, VerifyingEmail } from "./VerifyModal"; import jwtDecode from "jwt-decode"; const VerificationStep = { @@ -70,9 +70,9 @@ export function VerifyModalContainer() { const qs = new URLSearchParams(location.search); // auth_origin is not set when using OIDC flow so use a neutral default const origin = qs.get("auth_origin") || "Hubs"; - content = ; + content = ; } else { - content = ; + content = ; } return {content}; From dde99b80d48a4a6715225e7b416dcb1c35521c23 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 24 Oct 2022 08:30:39 +0100 Subject: [PATCH 28/32] Import order change to simplify PR --- src/react-components/auth/RoomSignInModalContainer.js | 2 +- src/react-components/auth/SignInModal.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react-components/auth/RoomSignInModalContainer.js b/src/react-components/auth/RoomSignInModalContainer.js index 961e845c42..009936d995 100644 --- a/src/react-components/auth/RoomSignInModalContainer.js +++ b/src/react-components/auth/RoomSignInModalContainer.js @@ -1,7 +1,7 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; import configs from "../../utils/configs"; -import { SignInModal, SignInStep, SubmitEmail, SubmitOIDC, WaitForVerification, SignInComplete } from "./SignInModal"; +import { SignInModal, SignInStep, SubmitEmail, WaitForVerification, SignInComplete, SubmitOIDC } from "./SignInModal"; import { TERMS, PRIVACY } from "../../constants"; // TODO: Migrate to use AuthContext diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index 1c154b378b..ce3323671b 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { CloseButton } from "../input/CloseButton"; import { Modal } from "../modal/Modal"; import { FormattedMessage, useIntl, defineMessages } from "react-intl"; -import { Button, CancelButton, NextButton, ContinueButton } from "../input/Button"; +import { CancelButton, NextButton, ContinueButton, Button } from "../input/Button"; import { TextInputField } from "../input/TextInputField"; import { Column } from "../layout/Column"; import { LegalMessage } from "./LegalMessage"; From 1913de2b764d21d9d7e03c21066e32969ee4b3ff Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 24 Oct 2022 08:31:00 +0100 Subject: [PATCH 29/32] More verification name reversions --- src/verify.html | 2 +- src/verify.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verify.html b/src/verify.html index 76d87b6e09..8ee0ea6e63 100644 --- a/src/verify.html +++ b/src/verify.html @@ -8,7 +8,7 @@ - Verify Account + Verify Email diff --git a/src/verify.js b/src/verify.js index 313e847553..85457a2664 100644 --- a/src/verify.js +++ b/src/verify.js @@ -12,7 +12,7 @@ import { PageContainer } from "./react-components/layout/PageContainer"; import { Center } from "./react-components/layout/Center"; import { ThemeProvider } from "./react-components/styles/theme"; -registerTelemetry("/verify", "Hubs Verify Account Page"); +registerTelemetry("/verify", "Hubs Verify Email Page"); const store = new Store(); window.APP = { store }; From 7543c7350f46cb8702cfb8cb8a35ff328c006818 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 24 Oct 2022 08:36:01 +0100 Subject: [PATCH 30/32] Reinstate verification window close from original spike --- src/react-components/auth/VerifyModal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/react-components/auth/VerifyModal.js b/src/react-components/auth/VerifyModal.js index dbc6c7368d..d01781024f 100644 --- a/src/react-components/auth/VerifyModal.js +++ b/src/react-components/auth/VerifyModal.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import PropTypes from "prop-types"; import { Spinner } from "../misc/Spinner"; import { Modal } from "../modal/Modal"; @@ -24,6 +24,10 @@ export function VerifyingEmail() { } export function EmailVerified({ origin }) { + // Close the window, but display a comfort message anyway + useEffect(function() { + window.close(); + }); return ( From 0eb0af6f3d7193ff3afcd0d42fad3f0f1609a580 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Mon, 24 Oct 2022 13:58:40 +0100 Subject: [PATCH 31/32] Fixed undefined variable --- src/react-components/auth/SignInModal.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index ce3323671b..419021f0b4 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -138,6 +138,8 @@ SubmitEmail.propTypes = { }; export function SubmitOIDC({ onSubmitOIDC, privacyUrl, termsUrl, message }) { + const intl = useIntl(); + const onSubmitForm = useCallback( e => { e.preventDefault(); From a1364b52dbfce0606a958144c0697ecbba18eb20 Mon Sep 17 00:00:00 2001 From: Rupert Rawnsley Date: Tue, 29 Nov 2022 09:54:24 +0000 Subject: [PATCH 32/32] Adjust indentation --- .../auth/VerifyModalContainer.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/react-components/auth/VerifyModalContainer.js b/src/react-components/auth/VerifyModalContainer.js index 9cde23e6d5..19768b81fe 100644 --- a/src/react-components/auth/VerifyModalContainer.js +++ b/src/react-components/auth/VerifyModalContainer.js @@ -19,28 +19,28 @@ function useVerify() { try { const qs = new URLSearchParams(location.search); - if (qs.get("error")) { - throw new Error(`${qs.get("error")}: ${qs.get("error_description")}`); - } - - let authParams; - if (qs.get("code")) { - const state = qs.get("state"); - const topic_key = jwtDecode(state).topic_key; - authParams = { - topic: `oidc:${topic_key}`, - token: qs.get("code"), - origin: "oidc", - payload: qs.get("state") - }; - } else { - authParams = { - topic: qs.get("auth_topic"), - token: qs.get("auth_token"), - origin: qs.get("auth_origin"), - payload: qs.get("auth_payload") - }; - } + if (qs.get("error")) { + throw new Error(`${qs.get("error")}: ${qs.get("error_description")}`); + } + + let authParams; + if (qs.get("code")) { + const state = qs.get("state"); + const topic_key = jwtDecode(state).topic_key; + authParams = { + topic: `oidc:${topic_key}`, + token: qs.get("code"), + origin: "oidc", + payload: qs.get("state") + }; + } else { + authParams = { + topic: qs.get("auth_topic"), + token: qs.get("auth_token"), + origin: qs.get("auth_origin"), + payload: qs.get("auth_payload") + }; + } await verify(authParams); setStep(VerificationStep.complete);