Download raw body.
gotwebd login status and logout link
On Sat, Feb 07, 2026 at 09:12:29PM +0100, Omar Polo wrote:
> Stefan Sperling <stsp@stsp.name> wrote:
> > Make gotwebd display the name of the logged in user, and add a link which
> > can be clicked to log out (asking the browser to delete the auth cookie).
>
> a nitpick is that GET should be idempotent, and so they can be cached,
> so it'd be better for the logout to be a form that POST somewhere.
>
> We don't actually have anything at the moment to deal with non-GET
> requests so it's probably fine.
>
> > ok?
>
> just one nit regarding how cookies are deleted, otherwise yes, ok op@
>
> > [...]
> > --- gotwebd/auth.c
> > +++ gotwebd/auth.c
> > @@ -420,6 +420,69 @@ err:
> > }
> > }
> >
> > +static void
> > +do_logout(struct request *c)
> > +{
>
> > [...]
>
> > + /* Ask the browser to delete the authentication cookie. */
> > + r = tp_writef(c->tp, "Clear-Site-Data: \"cookies\"\r\n");
>
> this is a bit of a too big hammer for the job we're trying to do.
> gotwebd might be running on the same domain as other software, and
> deleting all the cookies seems a bit rude.
>
> what we could do instead is setting the cookie to an invalid value and
> with an expired date, like:
>
> Set-Cookie: gtdauth=invalid; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT
>
> (actually, re-reading this i noticed that the fact that we're setting
> Path=/ is even slightly wrong, should be the prefix where gotwebd is
> actually running.)
My initial attempt at this used Set-Cookie but I preserved the auth
token value while setting a negatvive expity date. Somehow this was
not enough, and the Logout link didn't work (at least in Firefox, did
not try others). With some more fiddling I could get it work, see below.
I have fixed the Path attribute of the auth cookie, too. During testing I
found that the Set-Cookie Path must match in both Set-Cookie headers for
login and logout to work, so this seems to have the desired effect of
scoping the cookie to the pages generated by gotwebd.
ok?
M gotwebd/auth.c | 78+ 2-
M gotwebd/fcgi.c | 6+ 0-
M gotwebd/files/htdocs/gotwebd/gotweb.css | 9+ 0-
M gotwebd/gotweb.c | 6+ 0-
M gotwebd/gotwebd.h | 3+ 0-
M gotwebd/pages.tmpl | 12+ 1-
M gotwebd/sockets.c | 3+ 0-
M include/got_error.h | 1+ 0-
M lib/error.c | 1+ 0-
M regress/gotsysd/test_gotwebd.sh | 6+ 0-
M regress/gotsysd/test_gotwebd_repos_www.sh | 1+ 0-
11 files changed, 126 insertions(+), 3 deletions(-)
commit - cc8eaf00fec74bd43bc96a5daedef4c8540964bf
commit + 608470e5f07df5670a6f3176ef8ac65b8f610fd0
blob - 3e9cee3c275c4af7cd66b09401c857dac8177c95
blob + 72aab4aace8c3420df9a3caf8563af0f27433489
--- gotwebd/auth.c
+++ gotwebd/auth.c
@@ -388,9 +388,9 @@ logged_in:
}
r = tp_writef(c->tp, "Set-Cookie: gwdauth=%s;"
- " SameSite=Strict;%s Path=/; HttpOnly; Max-Age=%llu\r\n", token,
+ " SameSite=Strict;%s Path=%s; HttpOnly; Max-Age=%llu\r\n", token,
env->auth_config == GOTWEBD_AUTH_SECURE ? " Secure;" : "",
- validity);
+ srv->gotweb_url_root, validity);
explicit_bzero(token, strlen(token));
free(token);
if (r == -1) {
@@ -420,6 +420,80 @@ err:
}
}
+static void
+do_logout(struct request *c)
+{
+ const struct got_error *error = NULL;
+ struct gotwebd *env = gotwebd_env;
+ uid_t uid;
+ struct server *srv;
+ char *hostname = NULL;
+ const char *identifier = NULL;
+ struct gotweb_url url;
+
+ int r;
+
+ if (login_check_token(&uid, &hostname, c->fcgi_params.auth_cookie,
+ auth_token_secret, sizeof(auth_token_secret),
+ "authentication") == -1) {
+ error = got_error(GOT_ERR_LOGOUT_FAILED);
+ goto err;
+ }
+
+ /*
+ * The www user ID represents the case where no authentication
+ * occurred. This user must not be allowed to log in.
+ */
+ if (uid == env->www_uid) {
+ error = got_error(GOT_ERR_LOGOUT_FAILED);
+ goto err;
+ }
+
+ c->client_uid = uid;
+ if (strcmp(hostname, c->fcgi_params.server_name) != 0) {
+ error = got_error_msg(GOT_ERR_LOGOUT_FAILED,
+ "wrong server name in authentication cookie");
+ goto err;
+ }
+
+ srv = gotweb_get_server(c->fcgi_params.server_name);
+ if (srv == NULL) {
+ error = got_error_msg(GOT_ERR_LOGIN_FAILED,
+ "invalid server name for logout");
+ goto err;
+ }
+
+ if (gotwebd_env->gotwebd_verbose > 0) {
+ log_info("logging out uid %u as %s for server \"%s\"",
+ uid, identifier, hostname);
+ }
+
+ /* Ask the browser to delete the authentication cookie. */
+ r = tp_writef(c->tp, "Set-Cookie: gwdauth=invalid;"
+ " SameSite=Strict;%s Path=%s; HttpOnly; Max-Age=-1\r\n",
+ env->auth_config == GOTWEBD_AUTH_SECURE ? " Secure;" : "",
+ srv->gotweb_url_root);
+ if (r == -1) {
+ error = got_error_from_errno("tp_writef");
+ goto err;
+ }
+
+ memset(&url, 0, sizeof(url));
+ url.action = INDEX;
+ gotweb_reply(c, 307, "text/html", &url);
+ return;
+
+err:
+ free(hostname);
+ hostname = NULL;
+
+ log_warnx("%s: %s", __func__, error->msg);
+ c->t->error = error;
+ if (gotweb_reply(c, 400, "text/html", NULL) == -1)
+ return;
+ gotweb_render_page(c->tp, gotweb_render_error);
+}
+
static const struct got_error *
login_error_hint(struct request *c)
{
@@ -760,6 +834,8 @@ auth_dispatch_sockets(int fd, short event, void *arg)
if (c->fcgi_params.qs.login[0] != '\0')
do_login(c);
+ else if (c->fcgi_params.qs.logout)
+ do_logout(c);
else
process_request(c);
blob - 2a81e7f5e7e6523aa37d8d9e15a0ab325483eb37
blob + 3aa0806a14d767bd4f2ead5eea1fec881a7c5e9c
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -192,6 +192,7 @@ static const struct querystring_keys querystring_keys[
{ "index_page", GOTWEBD_QS_INDEX_PAGE },
{ "path", GOTWEBD_QS_PATH },
{ "login", GOTWEBD_QS_LOGIN },
+ { "logout", GOTWEBD_QS_LOGOUT },
};
static const struct action_keys action_keys[] = {
@@ -380,6 +381,11 @@ assign_querystring(struct querystring *qs, char *key,
goto done;
}
break;
+ case GOTWEBD_QS_LOGOUT:
+ if (strcmp(value, "1") != 0)
+ break;
+ qs->logout = 1;
+ break;
}
/* entry found */
blob - ea4cdedb51cb3f930bbc483b0f82bc9590a180b6
blob + 15e6ffef2c615352d5e23d6ef94fe1e37fb9dff1
--- gotwebd/files/htdocs/gotwebd/gotweb.css
+++ gotwebd/files/htdocs/gotwebd/gotweb.css
@@ -101,6 +101,15 @@ hr {
padding-bottom: 10px;
padding-top: 10px;
}
+#login_status {
+ color: lightgrey;
+ text-align: right;
+ padding-right: 10px;
+ padding-left: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ font-weight: monospace;
+}
#np_wrapper {
width: 100%;
border-bottom: 1px dotted #444444;
blob - 2dcdf486c002a5a2a948db88611b6912fc1de984
blob + 21603a5b4142f0537aca3e2741d0930cb22ef6b8
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -1469,6 +1469,12 @@ gotweb_render_url(struct request *c, struct gotweb_url
sep = "&";
}
+ if (url->logout == 1) {
+ if (tp_writef(c->tp, "%slogout=1", sep) == -1)
+ return -1;
+ sep = "&";
+ }
+
return 0;
}
blob - 69c2896f72735357062dc4f09af598567a4aa339
blob + e308c7906f34668208667e5f2742f917d90a6df7
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -311,6 +311,7 @@ struct querystring {
int index_page;
char path[PATH_MAX];
char login[MAX_AUTH_COOKIE];
+ int logout;
};
struct gotwebd_fcgi_params {
@@ -556,6 +557,7 @@ struct gotweb_url {
const char *folder;
const char *headref;
const char *path;
+ int logout;
};
struct querystring_keys {
@@ -577,6 +579,7 @@ enum querystring_elements {
GOTWEBD_QS_INDEX_PAGE,
GOTWEBD_QS_PATH,
GOTWEBD_QS_LOGIN,
+ GOTWEBD_QS_LOGOUT,
};
extern struct gotwebd *gotwebd_env;
blob - 4fb768f84e038e9bdfc9ee78a9e9c615084cb757
blob + 620eae4ecfde34d2e50eb56fa4eb94374d73fe0d
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -169,7 +169,7 @@ nextsep(char *s, char **t)
struct request *c = tp->tp_arg;
struct server *srv = c->srv;
const struct querystring *qs = c->t->qs;
- struct gotweb_url u_path;
+ struct gotweb_url u_path, u_logout;
char prefix[MAX_DOCUMENT_URI * 2];
const char *css = srv->custom_css;
const char *url_root, *url_path;
@@ -198,6 +198,11 @@ nextsep(char *s, char **t)
memset(&u_path, 0, sizeof(u_path));
u_path.index_page = -1;
u_path.action = SUMMARY;
+
+ memset(&u_logout, 0, sizeof(u_logout));
+ u_logout.index_page = -1;
+ u_logout.action = INDEX;
+ u_logout.logout = 1;
!}
<!doctype html>
<html>
@@ -221,6 +226,12 @@ nextsep(char *s, char **t)
<img src="{{ prefix }}/{{ srv->logo }}" />
</a>
</div>
+ {{ if (c->access_identifier[0]) }}
+ <div id="login_status">
+ Logged in as: <b>{{ c->access_identifier }}</b>
+ (<a href="{{ render gotweb_render_url(c, &u_logout) }}">Logout</a>)
+ </div>
+ {{ end }}
</header>
<nav id="site_path">
<div id="site_link">
blob - 387e6c57b105661f9840264b1240b2880aba9517
blob + 6d106f0a375678d96ece6c28b54c08e49d0cdecd
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -640,6 +640,9 @@ recv_parsed_params(struct imsg *imsg)
goto fail;
}
+ if (params.qs.logout)
+ p->qs.logout = 1;
+
if (params.document_uri[0] != '\0' &&
strlcpy(p->document_uri, params.document_uri,
sizeof(p->document_uri)) >= sizeof(p->document_uri)) {
blob - 1868cf47328cce63d6b5b9fec9cd8152af55ed29
blob + 301dcaa124592fd188562af5f52a713b9138d4b9
--- include/got_error.h
+++ include/got_error.h
@@ -201,6 +201,7 @@
#define GOT_ERR_UNKNOWN_COMMAND 193
#define GOT_ERR_NOT_FOUND 194
#define GOT_ERR_MEDIA_TYPE 195
+#define GOT_ERR_LOGOUT_FAILED 196
struct got_error {
int code;
blob - 6d25b5840fd9283ff9543c6405e6dca52a0825f8
blob + dcca96bf17d67b78004204c30d6fdf6ecd2153f5
--- lib/error.c
+++ lib/error.c
@@ -252,6 +252,7 @@ static const struct got_error got_errors[] = {
{ GOT_ERR_UNKNOWN_COMMAND, "command not found" },
{ GOT_ERR_NOT_FOUND, "not found" },
{ GOT_ERR_MEDIA_TYPE, "malformed media type" },
+ { GOT_ERR_LOGOUT_FAILED, "logout failed" },
};
static struct got_custom_error {
blob - b8c0500e50a21e234c5a8ec5f7dd2688296be03e
blob + eb1b65c4fa32ec243b7e7e8e4f29b25d9e336c16
--- regress/gotsysd/test_gotwebd.sh
+++ regress/gotsysd/test_gotwebd.sh
@@ -173,6 +173,7 @@ EOF
w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories
Repository
testrepo.git
@@ -202,6 +203,7 @@ EOF
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories / testrepo.git / tree /
Tree
@@ -401,6 +403,7 @@ EOF
w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories
Repository
gottest.git
@@ -435,6 +438,7 @@ EOF
w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_DEV_USER} (Logout)
Repositories
Repository
gottestdev.git
@@ -601,6 +605,7 @@ EOF
w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories
Repository
gottest.git
@@ -626,6 +631,7 @@ EOF
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories / gottest.git / tree /
reference refs/heads/main not found
blob - f649b0aa2de90feea176fb354a2986697d9e5826
blob + 56fb4c9aa170331283950ac27c8fdeb2db863c0b
--- regress/gotsysd/test_gotwebd_repos_www.sh
+++ regress/gotsysd/test_gotwebd_repos_www.sh
@@ -193,6 +193,7 @@ EOF
w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout
cat > $testroot/stdout.expected <<EOF
[got]
+Logged in as: ${GOTSYSD_TEST_USER} (Logout)
Repositories
Repository
www.git
gotwebd login status and logout link