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"
#
-----------------------------------------------------------------------------