diff --git a/server/server.go b/server/server.go
index b54a9d87..88be069f 100644
--- a/server/server.go
+++ b/server/server.go
@@ -862,7 +862,7 @@ func parseSince(r *http.Request, poll bool) (sinceTime, error) {
 func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
 	w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
 	w.Header().Set("Access-Control-Allow-Origin", "*")              // CORS, allow cross-origin requests
-	w.Header().Set("Access-Control-Allow-Headers", "Authorization") // CORS, allow auth
+	w.Header().Set("Access-Control-Allow-Headers", "Authorization") // CORS, allow auth via JS
 	return nil
 }
 
@@ -1091,7 +1091,7 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc {
 			return err
 		}
 		var user *auth.User // may stay nil if no auth header!
-		username, password, ok := r.BasicAuth()
+		username, password, ok := extractUserPass(r)
 		if ok {
 			if user, err = s.auth.Authenticate(username, password); err != nil {
 				log.Printf("authentication failed: %s", err.Error())
@@ -1108,6 +1108,27 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc {
 	}
 }
 
+// extractUserPass reads the username/password from the basic auth header (Authorization: Basic ...),
+// or from the ?auth=... query param. The latter is required only to support the WebSocket JavaScript
+// class, which does not support passing headers during the initial request. The auth query param
+// is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)).
+func extractUserPass(r *http.Request) (username string, password string, ok bool) {
+	username, password, ok = r.BasicAuth()
+	if ok {
+		return
+	}
+	authParam := readQueryParam(r, "authorization", "auth")
+	if authParam != "" {
+		a, err := base64.RawURLEncoding.DecodeString(authParam)
+		if err != nil {
+			return
+		}
+		r.Header.Set("Authorization", string(a))
+		return r.BasicAuth()
+	}
+	return
+}
+
 // visitor creates or retrieves a rate.Limiter for the given visitor.
 // This function was taken from https://www.alexedwards.net/blog/how-to-rate-limit-http-requests (MIT).
 func (s *Server) visitor(r *http.Request) *visitor {
diff --git a/server/server_test.go b/server/server_test.go
index 614cd5c9..dcc23650 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -657,6 +657,25 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
 	require.Equal(t, 403, response.Code) // Anonymous read not allowed
 }
 
+func TestServer_Auth_ViaQuery(t *testing.T) {
+	c := newTestConfig(t)
+	c.AuthFile = filepath.Join(t.TempDir(), "user.db")
+	c.AuthDefaultRead = false
+	c.AuthDefaultWrite = false
+	s := newTestServer(t, c)
+
+	manager := s.auth.(auth.Manager)
+	require.Nil(t, manager.AddUser("ben", "some pass", auth.RoleAdmin))
+
+	u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
+	response := request(t, s, "GET", u, "", nil)
+	require.Equal(t, 200, response.Code)
+
+	u = fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:WRONNNGGGG"))))
+	response = request(t, s, "GET", u, "", nil)
+	require.Equal(t, 401, response.Code)
+}
+
 /*
 func TestServer_Curl_Publish_Poll(t *testing.T) {
 	s, port := test.StartServer(t)
diff --git a/server/util.go b/server/util.go
index 08832dcf..7c596344 100644
--- a/server/util.go
+++ b/server/util.go
@@ -14,12 +14,24 @@ func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
 }
 
 func readParam(r *http.Request, names ...string) string {
+	value := readHeaderParam(r, names...)
+	if value != "" {
+		return value
+	}
+	return readQueryParam(r, names...)
+}
+
+func readHeaderParam(r *http.Request, names ...string) string {
 	for _, name := range names {
 		value := r.Header.Get(name)
 		if value != "" {
 			return strings.TrimSpace(value)
 		}
 	}
+	return ""
+}
+
+func readQueryParam(r *http.Request, names ...string) string {
 	for _, name := range names {
 		value := r.URL.Query().Get(strings.ToLower(name))
 		if value != "" {
diff --git a/web/src/app/Api.js b/web/src/app/Api.js
index dc43a296..ae21ad3c 100644
--- a/web/src/app/Api.js
+++ b/web/src/app/Api.js
@@ -1,31 +1,32 @@
-import {topicUrlJsonPoll, fetchLinesIterator, topicUrl, topicUrlAuth} from "./utils";
+import {topicUrlJsonPoll, fetchLinesIterator, topicUrl, topicUrlAuth, maybeWithBasicAuth} from "./utils";
 
 class Api {
-    async poll(baseUrl, topic) {
+    async poll(baseUrl, topic, user) {
         const url = topicUrlJsonPoll(baseUrl, topic);
         const messages = [];
+        const headers = maybeWithBasicAuth({}, user);
         console.log(`[Api] Polling ${url}`);
-        for await (let line of fetchLinesIterator(url)) {
+        for await (let line of fetchLinesIterator(url, headers)) {
             messages.push(JSON.parse(line));
         }
         return messages;
     }
 
-    async publish(baseUrl, topic, message) {
+    async publish(baseUrl, topic, user, message) {
         const url = topicUrl(baseUrl, topic);
         console.log(`[Api] Publishing message to ${url}`);
         await fetch(url, {
             method: 'PUT',
-            body: message
+            body: message,
+            headers: maybeWithBasicAuth({}, user)
         });
     }
 
     async auth(baseUrl, topic, user) {
         const url = topicUrlAuth(baseUrl, topic);
         console.log(`[Api] Checking auth for ${url}`);
-        const headers = this.maybeAddAuthorization({}, user);
         const response = await fetch(url, {
-            headers: headers
+            headers: maybeWithBasicAuth({}, user)
         });
         if (response.status >= 200 && response.status <= 299) {
             return true;
@@ -36,14 +37,6 @@ class Api {
         }
         throw new Error(`Unexpected server response ${response.status}`);
     }
-
-    maybeAddAuthorization(headers, user) {
-        if (user) {
-            const encoded = new Buffer(`${user.username}:${user.password}`).toString('base64');
-            headers['Authorization'] = `Basic ${encoded}`;
-        }
-        return headers;
-    }
 }
 
 const api = new Api();
diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js
index 914fcf45..fdf5c99f 100644
--- a/web/src/app/Connection.js
+++ b/web/src/app/Connection.js
@@ -1,14 +1,15 @@
-import {shortTopicUrl, topicUrlWs, topicUrlWsWithSince} from "./utils";
+import {basicAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
 
 const retryBackoffSeconds = [5, 10, 15, 20, 30, 45];
 
 class Connection {
-    constructor(subscriptionId, baseUrl, topic, since, onNotification) {
+    constructor(subscriptionId, baseUrl, topic, user, since, onNotification) {
         this.subscriptionId = subscriptionId;
         this.baseUrl = baseUrl;
         this.topic = topic;
+        this.user = user;
         this.since = since;
-        this.shortUrl = shortTopicUrl(baseUrl, topic);
+        this.shortUrl = topicShortUrl(baseUrl, topic);
         this.onNotification = onNotification;
         this.ws = null;
         this.retryCount = 0;
@@ -18,10 +19,10 @@ class Connection {
     start() {
         // Don't fetch old messages; we do that as a poll() when adding a subscription;
         // we don't want to re-trigger the main view re-render potentially hundreds of times.
-        const wsUrl = (this.since === 0)
-            ? topicUrlWs(this.baseUrl, this.topic)
-            : topicUrlWsWithSince(this.baseUrl, this.topic, this.since.toString());
+
+        const wsUrl = this.wsUrl();
         console.log(`[Connection, ${this.shortUrl}] Opening connection to ${wsUrl}`);
+
         this.ws = new WebSocket(wsUrl);
         this.ws.onopen = (event) => {
             console.log(`[Connection, ${this.shortUrl}] Connection established`, event);
@@ -75,6 +76,19 @@ class Connection {
         this.retryTimeout = null;
         this.ws = null;
     }
+
+    wsUrl() {
+        const params = [];
+        if (this.since > 0) {
+            params.push(`since=${this.since.toString()}`);
+        }
+        if (this.user !== null) {
+            const auth = encodeBase64Url(basicAuth(this.user.username, this.user.password));
+            params.push(`auth=${auth}`);
+        }
+        const wsUrl = topicUrlWs(this.baseUrl, this.topic);
+        return (params.length === 0) ? wsUrl : `${wsUrl}?${params.join('&')}`;
+    }
 }
 
 export default Connection;
diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js
index 374bba2b..67b41362 100644
--- a/web/src/app/ConnectionManager.js
+++ b/web/src/app/ConnectionManager.js
@@ -1,11 +1,11 @@
 import Connection from "./Connection";
 
-export class ConnectionManager {
+class ConnectionManager {
     constructor() {
         this.connections = new Map();
     }
 
-    refresh(subscriptions, onNotification) {
+    refresh(subscriptions, users, onNotification) {
         console.log(`[ConnectionManager] Refreshing connections`);
         const subscriptionIds = subscriptions.ids();
         const deletedIds = Array.from(this.connections.keys()).filter(id => !subscriptionIds.includes(id));
@@ -16,8 +16,9 @@ export class ConnectionManager {
             if (added) {
                 const baseUrl = subscription.baseUrl;
                 const topic = subscription.topic;
+                const user = users.get(baseUrl);
                 const since = 0;
-                const connection = new Connection(id, baseUrl, topic, since, onNotification);
+                const connection = new Connection(id, baseUrl, topic, user, since, onNotification);
                 this.connections.set(id, connection);
                 console.log(`[ConnectionManager] Starting new connection ${id}`);
                 connection.start();
diff --git a/web/src/app/Repository.js b/web/src/app/Repository.js
index 541e651d..72ebb11d 100644
--- a/web/src/app/Repository.js
+++ b/web/src/app/Repository.js
@@ -1,7 +1,9 @@
 import Subscription from "./Subscription";
 import Subscriptions from "./Subscriptions";
+import Users from "./Users";
+import User from "./User";
 
-export class Repository {
+class Repository {
     loadSubscriptions() {
         console.log(`[Repository] Loading subscriptions from localStorage`);
         const subscriptions = new Subscriptions();
@@ -10,8 +12,7 @@ export class Repository {
             return subscriptions;
         }
         try {
-            const serializedSubscriptions = JSON.parse(serialized);
-            serializedSubscriptions.forEach(s => {
+            JSON.parse(serialized).forEach(s => {
                 const subscription = new Subscription(s.baseUrl, s.topic);
                 subscription.addNotifications(s.notifications);
                 subscriptions.add(subscription);
@@ -39,26 +40,32 @@ export class Repository {
 
     loadUsers() {
         console.log(`[Repository] Loading users from localStorage`);
+        const users = new Users();
         const serialized = localStorage.getItem('users');
         if (serialized === null) {
-            return {};
+            return users;
         }
         try {
-            return JSON.parse(serialized);
+            JSON.parse(serialized).forEach(u => {
+                users.add(new User(u.baseUrl, u.username, u.password));
+            });
+            return users;
         } catch (e) {
             console.log(`[Repository] Unable to deserialize users: ${e.message}`);
-            return {};
+            return users;
         }
     }
 
-    saveUser(baseUrl, username, password) {
+    saveUsers(users) {
         console.log(`[Repository] Saving users to localStorage`);
-        const users = this.loadUsers();
-        users[baseUrl] = {
-            username: username,
-            password: password
-        };
-        localStorage.setItem('users', users);
+        const serialized = JSON.stringify(users.map(user => {
+            return {
+                baseUrl: user.baseUrl,
+                username: user.username,
+                password: user.password
+            }
+        }));
+        localStorage.setItem('users', serialized);
     }
 }
 
diff --git a/web/src/app/Subscription.js b/web/src/app/Subscription.js
index 8b19a18b..56b360d0 100644
--- a/web/src/app/Subscription.js
+++ b/web/src/app/Subscription.js
@@ -1,6 +1,6 @@
-import {shortTopicUrl, topicUrl} from './utils';
+import {topicShortUrl, topicUrl} from './utils';
 
-export default class Subscription {
+class Subscription {
     constructor(baseUrl, topic) {
         this.id = topicUrl(baseUrl, topic);
         this.baseUrl = baseUrl;
@@ -40,6 +40,8 @@ export default class Subscription {
     }
 
     shortUrl() {
-        return shortTopicUrl(this.baseUrl, this.topic);
+        return topicShortUrl(this.baseUrl, this.topic);
     }
 }
+
+export default Subscription;
diff --git a/web/src/app/User.js b/web/src/app/User.js
new file mode 100644
index 00000000..f92a83dc
--- /dev/null
+++ b/web/src/app/User.js
@@ -0,0 +1,9 @@
+class User {
+    constructor(baseUrl, username, password) {
+        this.baseUrl = baseUrl;
+        this.username = username;
+        this.password = password;
+    }
+}
+
+export default User;
diff --git a/web/src/app/Users.js b/web/src/app/Users.js
new file mode 100644
index 00000000..a795a23a
--- /dev/null
+++ b/web/src/app/Users.js
@@ -0,0 +1,36 @@
+class Users {
+    constructor() {
+        this.users = new Map();
+    }
+
+    add(user) {
+        this.users.set(user.baseUrl, user);
+        return this;
+    }
+
+    get(baseUrl) {
+        const user = this.users.get(baseUrl);
+        return (user) ? user : null;
+    }
+
+    update(user) {
+        return this.add(user);
+    }
+
+    remove(baseUrl) {
+        this.users.delete(baseUrl);
+        return this;
+    }
+
+    map(cb) {
+        return Array.from(this.users.values()).map(cb);
+    }
+
+    clone() {
+        const c = new Users();
+        c.users = new Map(this.users);
+        return c;
+    }
+}
+
+export default Users;
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 40b9f568..04847f2a 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -1,15 +1,14 @@
-import { rawEmojis} from "./emojis";
+import {rawEmojis} from "./emojis";
 
 export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
 export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`
     .replaceAll("https://", "wss://")
     .replaceAll("http://", "ws://");
-export const topicUrlWsWithSince = (baseUrl, topic, since) => `${topicUrlWs(baseUrl, topic)}?since=${since}`;
 export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`;
 export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
 export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
+export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
 export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
-export const shortTopicUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
 
 // Format emojis (see emoji.js)
 const emojis = {};
@@ -51,10 +50,35 @@ export const unmatchedTags = (tags) => {
     else return tags.filter(tag => !(tag in emojis));
 }
 
+
+export const maybeWithBasicAuth = (headers, user) => {
+    if (user) {
+        headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
+    }
+    return headers;
+}
+
+export const basicAuth = (username, password) => {
+    return `Basic ${encodeBase64(`${username}:${password}`)}`;
+}
+
+export const encodeBase64 = (s) => {
+    return new Buffer(s).toString('base64');
+}
+
+export const encodeBase64Url = (s) => {
+    return encodeBase64(s)
+        .replaceAll('+', '-')
+        .replaceAll('/', '_')
+        .replaceAll('=', '');
+}
+
 // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
-export async function* fetchLinesIterator(fileURL) {
+export async function* fetchLinesIterator(fileURL, headers) {
     const utf8Decoder = new TextDecoder('utf-8');
-    const response = await fetch(fileURL);
+    const response = await fetch(fileURL, {
+        headers: headers
+    });
     const reader = response.body.getReader();
     let { value: chunk, done: readerDone } = await reader.read();
     chunk = chunk ? utf8Decoder.decode(chunk) : '';
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index 7ecce8e4..05806478 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -25,6 +25,7 @@ const ActionBar = (props) => {
                 <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>{title}</Typography>
                 {props.selectedSubscription !== null && <IconSubscribeSettings
                     subscription={props.selectedSubscription}
+                    users={props.users}
                     onClearAll={props.onClearAll}
                     onUnsubscribe={props.onUnsubscribe}
                 />}
diff --git a/web/src/components/App.js b/web/src/components/App.js
index 51097833..d7aea030 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -12,12 +12,14 @@ import connectionManager from "../app/ConnectionManager";
 import Subscriptions from "../app/Subscriptions";
 import Navigation from "./Navigation";
 import ActionBar from "./ActionBar";
+import Users from "../app/Users";
 
 const App = () => {
     console.log(`[App] Rendering main view`);
 
     const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
     const [subscriptions, setSubscriptions] = useState(new Subscriptions());
+    const [users, setUsers] = useState(new Users());
     const [selectedSubscription, setSelectedSubscription] = useState(null);
     const handleNotification = (subscriptionId, notification) => {
         setSubscriptions(prev => {
@@ -25,11 +27,14 @@ const App = () => {
             return prev.update(newSubscription).clone();
         });
     };
-    const handleSubscribeSubmit = (subscription) => {
+    const handleSubscribeSubmit = (subscription, user) => {
         console.log(`[App] New subscription: ${subscription.id}`);
+        if (user !== null) {
+            setUsers(prev => prev.add(user).clone());
+        }
         setSubscriptions(prev => prev.add(subscription).clone());
         setSelectedSubscription(subscription);
-        api.poll(subscription.baseUrl, subscription.topic)
+        api.poll(subscription.baseUrl, subscription.topic, user)
             .then(messages => {
                 setSubscriptions(prev => {
                     const newSubscription = prev.get(subscription.id).addNotifications(messages);
@@ -61,12 +66,13 @@ const App = () => {
     };
     useEffect(() => {
         setSubscriptions(repository.loadSubscriptions());
+        setUsers(repository.loadUsers());
     }, [/* initial render only */]);
     useEffect(() => {
-        connectionManager.refresh(subscriptions, handleNotification);
+        connectionManager.refresh(subscriptions, users, handleNotification);
         repository.saveSubscriptions(subscriptions);
-    }, [subscriptions]);
-
+        repository.saveUsers(users);
+    }, [subscriptions, users]);
     return (
         <ThemeProvider theme={theme}>
             <CssBaseline/>
@@ -74,6 +80,7 @@ const App = () => {
                 <CssBaseline/>
                 <ActionBar
                     selectedSubscription={selectedSubscription}
+                    users={users}
                     onClearAll={handleDeleteAllNotifications}
                     onUnsubscribe={handleUnsubscribe}
                     onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
diff --git a/web/src/components/IconSubscribeSettings.js b/web/src/components/IconSubscribeSettings.js
index a3f18779..c8a3603a 100644
--- a/web/src/components/IconSubscribeSettings.js
+++ b/web/src/components/IconSubscribeSettings.js
@@ -14,6 +14,7 @@ import api from "../app/Api";
 const IconSubscribeSettings = (props) => {
     const [open, setOpen] = useState(false);
     const anchorRef = useRef(null);
+    const users = props.users;
 
     const handleToggle = () => {
         setOpen((prevOpen) => !prevOpen);
@@ -39,7 +40,9 @@ const IconSubscribeSettings = (props) => {
     const handleSendTestMessage = () => {
         const baseUrl = props.subscription.baseUrl;
         const topic = props.subscription.topic;
-        api.publish(baseUrl, topic, `This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
+        const user = users.get(baseUrl); // May be null
+        api.publish(baseUrl, topic, user,
+            `This is a test notification sent by the ntfy Web UI at ${new Date().toString()}.`); // FIXME result ignored
         setOpen(false);
     }
 
diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js
index 5d8b6e7e..2fc29d23 100644
--- a/web/src/components/Navigation.js
+++ b/web/src/components/Navigation.js
@@ -54,10 +54,15 @@ const Navigation = (props) => {
 Navigation.width = navWidth;
 
 const NavList = (props) => {
+    const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
     const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false);
-    const handleSubscribeSubmit = (subscription) => {
+    const handleSubscribeReset = () => {
         setSubscribeDialogOpen(false);
-        props.onSubscribeSubmit(subscription);
+        setSubscribeDialogKey(prev => prev+1);
+    }
+    const handleSubscribeSubmit = (subscription, user) => {
+        handleSubscribeReset();
+        props.onSubscribeSubmit(subscription, user);
     }
     return (
         <>
@@ -85,13 +90,15 @@ const NavList = (props) => {
                 </ListItemButton>
             </List>
             <SubscribeDialog
+                key={subscribeDialogKey} // Resets dialog when canceled/closed
                 open={subscribeDialogOpen}
-                onCancel={() => setSubscribeDialogOpen(false)}
-                onSubmit={handleSubscribeSubmit}
+                onCancel={handleSubscribeReset}
+                onSuccess={handleSubscribeSubmit}
             />
         </>
     );
 };
+
 const NavSubscriptionList = (props) => {
     const subscriptions = props.subscriptions;
     return (
diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js
index 20b3b0fd..6e34151c 100644
--- a/web/src/components/SubscribeDialog.js
+++ b/web/src/components/SubscribeDialog.js
@@ -13,6 +13,7 @@ import theme from "./theme";
 import api from "../app/Api";
 import {topicUrl} from "../app/utils";
 import useStyles from "./styles";
+import User from "../app/User";
 
 const defaultBaseUrl = "http://127.0.0.1"
 //const defaultBaseUrl = "https://ntfy.sh"
@@ -20,43 +21,50 @@ const defaultBaseUrl = "http://127.0.0.1"
 const SubscribeDialog = (props) => {
     const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
     const [topic, setTopic] = useState("");
-    const [user, setUser] = useState(null);
     const [showLoginPage, setShowLoginPage] = useState(false);
     const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
     const handleCancel = () => {
         setTopic('');
         props.onCancel();
     }
-    const handleSubmit = async () => {
-        const success = await api.auth(baseUrl, topic, null);
-        if (!success) {
-            console.log(`[SubscribeDialog] Login required for ${topicUrl(baseUrl, topic)}`)
-            setShowLoginPage(true);
-            return;
-        }
-        const subscription = new Subscription(defaultBaseUrl, topic);
-        props.onSubmit(subscription);
+    const handleSuccess = (baseUrl, topic, user) => {
+        const subscription = new Subscription(baseUrl, topic);
+        props.onSuccess(subscription, user);
         setTopic('');
     }
     return (
         <Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
             {!showLoginPage && <SubscribePage
+                baseUrl={baseUrl}
                 topic={topic}
                 setTopic={setTopic}
                 onCancel={handleCancel}
-                onSubmit={handleSubmit}
+                onNeedsLogin={() => setShowLoginPage(true)}
+                onSuccess={handleSuccess}
             />}
             {showLoginPage && <LoginPage
                 baseUrl={baseUrl}
                 topic={topic}
                 onBack={() => setShowLoginPage(false)}
-                onSubmit={handleSubmit}
+                onSuccess={handleSuccess}
             />}
         </Dialog>
     );
 };
 
 const SubscribePage = (props) => {
+    const baseUrl = props.baseUrl;
+    const topic = props.topic;
+    const handleSubscribe = async () => {
+        const success = await api.auth(baseUrl, topic, null);
+        if (!success) {
+            console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for anonymous user`);
+            props.onNeedsLogin();
+            return;
+        }
+        console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`);
+        props.onSuccess(baseUrl, topic, null);
+    };
     return (
         <>
             <DialogTitle>Subscribe to topic</DialogTitle>
@@ -79,7 +87,7 @@ const SubscribePage = (props) => {
             </DialogContent>
             <DialogActions>
                 <Button onClick={props.onCancel}>Cancel</Button>
-                <Button onClick={props.onSubmit} disabled={props.topic === ""}>Subscribe</Button>
+                <Button onClick={handleSubscribe} disabled={props.topic === ""}>Subscribe</Button>
             </DialogActions>
         </>
     );
@@ -93,14 +101,15 @@ const LoginPage = (props) => {
     const baseUrl = props.baseUrl;
     const topic = props.topic;
     const handleLogin = async () => {
-        const user = {username: username, password: password};
+        const user = new User(baseUrl, username, password);
         const success = await api.auth(baseUrl, topic, user);
         if (!success) {
             console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
             setErrorText(`User ${username} not authorized`);
             return;
         }
-        console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} successful for user ${username}`);
+        console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
+        props.onSuccess(baseUrl, topic, user);
     };
     return (
         <>