IndieAuth support (#1811)
* Able to authenticate user against IndieAuth. For #1273 * WIP server indieauth endpoint. For https://github.com/owncast/owncast/issues/1272 * Add migration to remove access tokens from user * Add authenticated bool to user for display purposes * Add indieauth modal and auth flair to display names. For #1273 * Validate URLs and display errors * Renames, cleanups * Handle relative auth endpoint paths. Add error handling for missing redirects. * Disallow using display names in use by registered users. Closes #1810 * Verify code verifier via code challenge on callback * Use relative path to authorization_endpoint * Post-rebase fixes * Use a timestamp instead of a bool for authenticated * Propertly handle and display error in modal * Use auth'ed timestamp to derive authenticated flag to display in chat * don't redirect unless a URL is present avoids redirecting to `undefined` if there was an error * improve error message if owncast server URL isn't set * fix IndieAuth PKCE implementation use SHA256 instead of SHA1, generates a longer code verifier (must be 43-128 chars long), fixes URL-safe SHA256 encoding * return real profile data for IndieAuth response * check the code verifier in the IndieAuth server * Linting * Add new chat settings modal anad split up indieauth ui * Remove logging error * Update the IndieAuth modal UI. For #1273 * Add IndieAuth repsonse error checking * Disable IndieAuth client if server URL is not set. * Add explicit error messages for specific error types * Fix bad logic * Return OAuth-keyed error responses for indieauth server * Display IndieAuth error in plain text with link to return to main page * Remove redundant check * Add additional detail to error * Hide IndieAuth details behind disclosure details * Break out migration into two steps because some people have been runing dev in production * Add auth option to user dropdown Co-authored-by: Aaron Parecki <aaron@parecki.com>
This commit is contained in:
@@ -27,6 +27,8 @@ import FediverseFollowModal, {
|
||||
|
||||
import { NotifyButton, NotifyModal } from './components/notification.js';
|
||||
import { isPushNotificationSupported } from './notification/registerWeb.js';
|
||||
import ChatSettingsModal from './components/chat-settings-modal.js';
|
||||
|
||||
import {
|
||||
addNewlines,
|
||||
checkUrlPathForDisplay,
|
||||
@@ -110,6 +112,9 @@ export default class App extends Component {
|
||||
externalActionModalData: null,
|
||||
fediverseModalData: null,
|
||||
|
||||
// authentication options
|
||||
indieAuthEnabled: false,
|
||||
|
||||
// routing & tabbing
|
||||
section: '',
|
||||
sectionId: '',
|
||||
@@ -144,6 +149,8 @@ export default class App extends Component {
|
||||
this.closeFediverseFollowModal = this.closeFediverseFollowModal.bind(this);
|
||||
this.displayNotificationModal = this.displayNotificationModal.bind(this);
|
||||
this.closeNotificationModal = this.closeNotificationModal.bind(this);
|
||||
this.showAuthModal = this.showAuthModal.bind(this);
|
||||
this.closeAuthModal = this.closeAuthModal.bind(this);
|
||||
|
||||
// player events
|
||||
this.handlePlayerReady = this.handlePlayerReady.bind(this);
|
||||
@@ -268,8 +275,14 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
setConfigData(data = {}) {
|
||||
const { name, summary, chatDisabled, socketHostOverride, notifications } =
|
||||
data;
|
||||
const {
|
||||
name,
|
||||
summary,
|
||||
chatDisabled,
|
||||
socketHostOverride,
|
||||
notifications,
|
||||
authentication,
|
||||
} = data;
|
||||
window.document.title = name;
|
||||
|
||||
this.socketHostOverride = socketHostOverride;
|
||||
@@ -281,10 +294,12 @@ export default class App extends Component {
|
||||
}
|
||||
|
||||
this.hasConfiguredChat = true;
|
||||
const { indieAuthEnabled } = authentication;
|
||||
|
||||
this.setState({
|
||||
canChat: !chatDisabled,
|
||||
notifications,
|
||||
indieAuthEnabled,
|
||||
configData: {
|
||||
...data,
|
||||
summary: summary && addNewlines(summary),
|
||||
@@ -618,6 +633,17 @@ export default class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
showAuthModal() {
|
||||
const data = {
|
||||
title: 'Chat',
|
||||
};
|
||||
this.setState({ authModalData: data });
|
||||
}
|
||||
|
||||
closeAuthModal() {
|
||||
this.setState({ authModalData: null });
|
||||
}
|
||||
|
||||
handleWebsocketMessage(e) {
|
||||
if (e.type === SOCKET_MESSAGE_TYPES.ERROR_USER_DISABLED) {
|
||||
// User has been actively disabled on the backend. Turn off chat for them.
|
||||
@@ -637,10 +663,10 @@ export default class App extends Component {
|
||||
// When connected the user will return an event letting us know what our
|
||||
// user details are so we can display them properly.
|
||||
const { user } = e;
|
||||
const { displayName } = user;
|
||||
|
||||
const { displayName, authenticated } = user;
|
||||
this.setState({
|
||||
username: displayName,
|
||||
authenticated,
|
||||
isModerator: checkIsModerator(e),
|
||||
});
|
||||
}
|
||||
@@ -724,17 +750,20 @@ export default class App extends Component {
|
||||
streamTitle,
|
||||
touchKeyboardActive,
|
||||
username,
|
||||
authenticated,
|
||||
viewerCount,
|
||||
websocket,
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
fediverseModalData,
|
||||
authModalData,
|
||||
externalActionModalData,
|
||||
notificationModalData,
|
||||
notifications,
|
||||
lastDisconnectTime,
|
||||
section,
|
||||
sectionId,
|
||||
indieAuthEnabled,
|
||||
} = state;
|
||||
|
||||
const {
|
||||
@@ -864,11 +893,32 @@ export default class App extends Component {
|
||||
/>`}
|
||||
/>`;
|
||||
|
||||
const authModal =
|
||||
authModalData &&
|
||||
html`
|
||||
<${ExternalActionModal}
|
||||
onClose=${this.closeAuthModal}
|
||||
action=${authModalData}
|
||||
useIframe=${false}
|
||||
customContent=${html`<${ChatSettingsModal}
|
||||
name=${name}
|
||||
logo=${logo}
|
||||
onUsernameChange=${this.handleUsernameChange}
|
||||
username=${username}
|
||||
accessToken=${this.state.accessToken}
|
||||
authenticated=${authenticated}
|
||||
onClose=${this.closeAuthModal}
|
||||
indieAuthEnabled=${indieAuthEnabled}
|
||||
/>`}
|
||||
/>
|
||||
`;
|
||||
|
||||
const chat = this.state.websocket
|
||||
? html`
|
||||
<${Chat}
|
||||
websocket=${websocket}
|
||||
username=${username}
|
||||
authenticated=${authenticated}
|
||||
chatInputEnabled=${chatInputEnabled && !chatDisabled}
|
||||
instanceTitle=${name}
|
||||
accessToken=${accessToken}
|
||||
@@ -911,6 +961,8 @@ export default class App extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
const authIcon = '/img/user-settings.svg';
|
||||
|
||||
return html`
|
||||
<div
|
||||
id="app-container"
|
||||
@@ -942,9 +994,11 @@ export default class App extends Component {
|
||||
>
|
||||
</h1>
|
||||
|
||||
<${ChatMenu} username=${username} isModerator=${isModerator} onUsernameChange=${
|
||||
this.handleUsernameChange
|
||||
} onFocus=${this.handleFormFocus} onBlur=${
|
||||
<${ChatMenu} username=${username} isModerator=${isModerator} showAuthModal=${
|
||||
indieAuthEnabled && this.showAuthModal
|
||||
} onUsernameChange=${this.handleUsernameChange} onFocus=${
|
||||
this.handleFormFocus
|
||||
} onBlur=${
|
||||
this.handleFormBlur
|
||||
} chatDisabled=${chatDisabled} noVideoContent=${noVideoContent} handleChatPanelToggle=${
|
||||
this.handleChatPanelToggle
|
||||
@@ -1027,7 +1081,7 @@ export default class App extends Component {
|
||||
</footer>
|
||||
|
||||
${chat} ${externalActionModal} ${fediverseFollowModal}
|
||||
${notificationModal}
|
||||
${notificationModal} ${authModal}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user