Hi,

I'm moving this conversation from the subversion dev mailing list for now
I'm working on adding 2FA to subversion

I have a working PoC of this system, and I'd like to start submitting patches.
The serf changes are attached, what is the process to get approval for this?

The patches, configs and scripts to make this work are publicly available here:
https://pub.svnplus.com/repo/2fa/trunk

The implementation uses a special 499 http response code to request 2FA auth, I have a working server config that serves this without any server side changes https://pub.svnplus.com/repo/2fa/trunk/webdav.conf

The system is running at https://2fa.svnplus.com/repo/2fa/trunk/, and it works in the browser and the svn cli with the patches applied, but the svn client does not yet save the cookie, so every command needs a 2fa

To access the demo system, use the username: user and password: pass
The 2FA QR code is displayed on the page

Please let me know, what you think

Best regards,
Peter

On 2025. 04. 02. 9:25, Daniel Sahlberg wrote:
Hi,

Den mån 31 mars 2025 kl 13:09 skrev Peter Balogh <pe...@svnplus.com>:

    Hi,

    I'm working on a proof of concept to add multi factor
    authentication to
    subversion
    I have multiple directions, and I'd like to get some feedback from
    this
    group before I head down the wrong road
    Please let me know, if I should read up on any of these, I did not
    find
    any recent conversation about these topics


I think this is a very interesting concept!


    1. One option would be to extend the existing Basic auth with another
    challenge. This would support standard TOTP without external
    connections
    I couldn't find any standard way to communicating this through
    HTTP, my
    best solution would be to

    1.A extend the 401 response with information that the system also
    needs
    a TOTP token
    I've only found the realm string I can use for this without modifying
    the serf library

    1.B reject the username + password attempt with a different 401
    This would have to be handled inside the serf library


I don't see a problem modifying Serf if it is needed. Several of the Subversion committers are also Serf committers and we can help there.


    2. Provide an external login path instead Basic auth. This would
    open up
    using oauth or similar external providers, but a web based provider
    could be run besides svn as well
    The server would respond with a 30X response, and the cmdline could
    print the url for the user to open, login and return to the terminal
    after authentication
    This would feel less native, but pretty common these days


I like this idea - I guess this would open up to for example Microsoft Entra.

Make sure to use the standard baton/callback method to communicate the redirect URL to the client. This way GUI clients (for example TortoiseSVN) can implement a more "native" solution to the redirect.


    All of the solutions above would result in a HTTP cookie that
    would be
    stored encrypted in the auth folder, and sent with every
    subsequent request


See existing discussions about storing the basic auth password in the auth folder. On Windows, there are APIs to encrypt the password linked to the user profile. In general no such on Unix, but I assume it could be stored in Gnome Keyring/Kwallet/GPG-agent.


    Do you have any thoughts or preferences about this?
    Do you see any technical or security issue with these?


I know very little about the technical details of authentication on the http level but I think it would be god to adhere to existing standards as much as possible instead of inventing something unique to Subversion. For example it would be good if this can be done across a forward/reverse proxy.

If this feature is enabled on a server, it would be impossible to authenticate with an older client. Don't know if this is a compatibility concern or not, but I think this addition would warrant new releases.

Kind regards,
Daniel
Index: auth/auth.c
===================================================================
--- auth/auth.c (revision 1925381)
+++ auth/auth.c (working copy)
@@ -216,8 +216,33 @@
                                   serf_bucket_t *response,
                                   apr_pool_t *pool)
 {
+    serf_bucket_t* hdrs;
+    if (code == 499) {
+        serf_connection_t* conn = request->conn;
+        serf_context_t* ctx = conn->ctx;
+        apr_status_t status;
+        hdrs = serf_bucket_response_get_headers(response);
+        const char *location = serf_bucket_headers_get(hdrs, "Location");
+
+        char* url;
+        if (location[0] == '/') {
+            url = apr_psprintf(pool, "%s://%s:%d%s",
+                               conn->host_info.scheme,
+                               conn->host_info.hostname,
+                               conn->host_info.port,
+                               location);
+        }
+        else {
+            url = apr_pstrdup(pool, location);
+        }
+
+        status = ctx->otp_cb(request, request->auth_baton, url, pool);
+        if (status)
+            return status;
+
+        return APR_SUCCESS;
+    }
     if (code == 401 || code == 407) {
-        serf_bucket_t *hdrs;
         auth_baton_t ab = { 0 };
 
         ab.hdrs = apr_hash_make(pool);
@@ -311,7 +336,7 @@
         return APR_SUCCESS;
     }
 
-    if (sl.code == 401 || sl.code == 407) {
+    if (sl.code == 401 || sl.code == 407 || sl.code == 499) {
         /* Authentication requested. */
 
         /* Don't bother handling the authentication request if the response
Index: serf.h
===================================================================
--- serf.h      (revision 1925381)
+++ serf.h      (working copy)
@@ -454,6 +454,17 @@
     apr_pool_t *pool);
 
 /**
+ * Callback function to be implemented by the application, so that serf
+ * can handle otp authentication.
+ * code = 499.
+ * baton = the baton passed to serf_context_run.
+ */
+typedef apr_status_t (*serf_otp_callback_t)(
+    serf_request_t *request, void *baton,
+    const char *auth_url,
+    apr_pool_t *pool);
+
+/**
  * Create a new connection associated with the @a ctx serf context.
  *
  * If no proxy server is configured, a connection will be created to
@@ -915,6 +926,13 @@
     serf_context_t *ctx,
     serf_credentials_callback_t cred_cb);
 
+/**
+ * Set the credentials callback handler.
+ */
+void serf_config_otp_callback(
+    serf_context_t *ctx,
+    serf_otp_callback_t cred_cb);
+
 /* ### maybe some connection control functions for flood? */
 
 /*** Special bucket creation functions ***/
Index: serf_private.h
===================================================================
--- serf_private.h      (revision 1925381)
+++ serf_private.h      (working copy)
@@ -421,6 +421,8 @@
     int authn_types;
     /* Callback function used to get credentials for a realm. */
     serf_credentials_callback_t cred_cb;
+    /* Callback function used to get otp for a realm. */
+    serf_otp_callback_t otp_cb;
 
     serf_config_t *config;
 };
Index: src/context.c
===================================================================
--- src/context.c       (revision 1925381)
+++ src/context.c       (working copy)
@@ -129,6 +129,13 @@
 }
 
 
+void serf_config_otp_callback(serf_context_t* ctx,
+    serf_otp_callback_t otp_cb)
+{
+    ctx->otp_cb = otp_cb;
+}
+
+
 void serf_config_authn_types(serf_context_t *ctx,
                              int authn_types)
 {

Reply via email to