The bash script below is a proof-of-concept for adding OAuth2/OIDC functionality to svn. *Question: is this enough to get someone interested in adding this functionality to svn?*

This follows on from two discussions on TortoiseSVN-dev <https://groups.google.com/g/tortoisesvn-dev/c/ByECclvGKi8> and us...@subversion.apache.org <https://lists.apache.org/thread/hzbzw1yr6z4zt6z1jffpdczm9qdvpt6q>. There may be some funding available (see the TortoiseSVN post), but I personally don't use TSVN, and I have no idea if there's any common code between TSVN and svn.

The script is straightforward, and attempts to retrieve a single named file:

1. It does a plain curl GET for the protected file, to confirm that a
   401 is returned
2. It runs oidc-agent <https://github.com/indigo-dc/oidc-agent> to get
   an access token
3. It repeats the first curl GET, but this time sends the access token

The curl request in Step 3 actually sets 'authorization: basic', with the bearer token in the credentials, rather than 'authorization: bearer'. This feels wrong (I haven't checked the RFC in detail), but this is what git-credential-oauth <https://github.com/hickford/git-credential-oauth> does, and it works in my setup (for git).

The heavy lifting is done by oidc-agent. This is a cross-platform command-line tool, written in C (I've only tried it on Linux). You need to create a small file to identify your OP (OpenID Provider, or IdP), which is encrypted by oidc-agent. When you ask oidc-agent for a token from this OP, it pops up a browser window , and connects to the OP, where you can log in (with 2FA/MFA if necessary). The OP then redirects back to oidc-agent, which needs to listen for this connection. The process of getting the access token is fairly complex. The token returned by oidc-agent is a JWT, coding a bearer token.

oidc-agent isn't the only alternative here, but the only other similar programs I know about are GCM <https://github.com/git-ecosystem/git-credential-manager> and git-credential-oauth, which are both git-specific. GCM is C#/.NET Core. It's rolled into Git for Windows, but is apparently difficult to get running on Linux. git-credential-oauth is also cross-platform, is written in Go, and is easy to run on Linux; I use it for Git access on the same server that I've tested this script on.

The setup that I'm testing on is as follows:

1. The svn (and git) repos are behind Apache (httpd), with
   mod_auth_openidc <https://github.com/OpenIDC/mod_auth_openidc>. This
   module is an OIDC Relying Party (RP). It will either connect to an
   OP itself to check authorisation or, as in this case, it will check
   incoming access/bearer tokens by connecting to the OP to carry out
   'introspection'
2. Apache grants access using 'require valid_user' and 'require
   oauth2_claim' for finer granularity
3. The identity provider is Keycloak, which also runs on the same
   server. I haven't tested with any other OPs (Google, Azure, GitHub, etc)

So, the script authenticates by connecting to Keycloak; on success, Keycloak returns a JWT to oidc-agent, and this JWT contains the user's roles/claims. The token is then sent to Apache by the script, and Apache checks the claims, and grants or denies access.

This particular server is rebuilt every couple of weeks. However, if anyone wants to look at this further I can set up a VPS with Keycloak and Apache/mod_auth_openidc, together with svn and git repos (the git one is useful for sanity testing and checking what's actually needed on the line).

-Evan

------------------------------------------------------------------------
#!/bin/bash
#
# File          : $HeadURL: http://localhost/svn/vserver/doc/notes/scripts/svn1.sh $
# Date          : $Date: 2024-04-02 19:14:00 +0000 (Tue, 02 Apr 2024) $
# File Revision : $Revision: 136 $
# Author        : Evan Lavelle
#
# Access an OAuth2-protected resource, 101: get an access token, and supply it
# to the server to retrieve a named file.
#
# -----------------------------------------------------------------------------

scriptname=$(basename "$0")

usage() {
    echo "Usage: $scriptname shortname file"
    echo ""
    echo "Fetch 'file' from an OAuth2-protected repository. 'file' should be"     echo "the full URL for the file;  this is the URL that you would use when"     echo "browsing the repo, for example.  'shortname' should  the oidc-agent"     echo "shortname  for the OpenID Provider  (or, alternatively, the  issuer"
    echo "URL)."
    echo "oidc-agent must be installed, and is used to fetch an access token."     echo "If necessary, you will  be asked to sign in at the OP. The provider"     echo "definition (which includes the client secret) is encrypted by oidc-"
    echo "agent, and you may also be asked to supply the decryption key."
    exit 1
}

if (( $# != 2)); then
    usage
fi

shortname=$1
file=$2

# (1) request the protected resource without supplying an access token, to
#     confirm that it can't be done. we need a '401 Unauthorized' to continue
output=$(curl -is  \
  -H "Accept: */* " \
  -H "Accept-Encoding: deflate, gzip, br, zstd " \
  -H "Accept-Language: en-GB, *;q=0.9 " \
  -H "Pragma: no-cache " \
  -X GET "$file" 2> /dev/null)

# get the HTTP response status code from 'output' (ie. 3 decimal digits in the
# first line)
status=$(echo "$output" | head -1 | grep -oP '[0-9]{3}')
if ((status >= 200 && status <= 299)); then
    printf "File '%s' is not a protected resource; terminating.\n" "$file"
    exit 2
elif ((status != 401)); then
    printf "Invalid HTTP status (%d) when fetching '%s'; terminating.\n" "$status" "$file"
    exit 3
fi

# (2) we have a 401: get an access token from oidc-agent
if ! token=$(oidc-token "$shortname" 2> /dev/null)
then
    printf "Could not run oidc-token ('%s')" "$token"
    exit 4
fi

# (3) base64-encode the token, with a username of 'oauth2'
if ! auth=$(printf "oauth2:%s" "$token" | base64 --wrap=0); then
    printf "Could not base64-encode the token ('%s')\n" "$auth"
    exit 5
fi

# (4) now repeat the original request with the access token
output=$(curl -is \
  -H "Accept: */* " \
  -H "Accept-Encoding: deflate, gzip, br, zstd " \
  -H "Accept-Language: en-GB, *;q=0.9 " \
  -H "Pragma: no-cache "                \
  -H "Authorization: basic $auth"       \
  -X GET "$file" 2> /dev/null)

# get the response status: if it's 2xx, we have the file
status=$(echo "$output" | head -1 | grep -oP '[0-9]{3}')
if ((status == 403)); then
    echo "You are not authorised to access this file. "
    echo "Do you have the relevant claims?"
    exit 6
elif ((status < 200 || status > 299)); then
    printf "File request failed ('%s')" "$output"
    exit 7
fi

# strip the headers
body=$(echo "$output" | perl -0777 -ne 's/.*\r\n\r\n(.*)/\1/s && print')

echo "------------------"
echo "The file contents:"
echo "------------------"
echo "$body"

# -----------------------------------------------------------------------------

Reply via email to