Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit b40169e

Browse files
committed
bundle-server: implement simple user/pass auth middleware
Create and test a built-in auth mode for a single fixed username/password authenticated against a provided 'Authorization: Basic' header, implementing the AuthMiddleware interface. Add the mode to 'parseAuthConfig()' in 'git-bundle-web-server' to enable its usage in the web server. Update documentation for the mode in 'docs/technical/auth-config.md' and 'git-bundle-web-server' manpage, and an explicit example JSON in the 'examples/auth/config' directory. Signed-off-by: Victoria Dye <vdye@github.com>
1 parent 6426168 commit b40169e

7 files changed

Lines changed: 396 additions & 1 deletion

File tree

cmd/git-bundle-web-server/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/git-ecosystem/git-bundle-server/cmd/utils"
1212
"github.com/git-ecosystem/git-bundle-server/internal/argparse"
13+
auth_internal "github.com/git-ecosystem/git-bundle-server/internal/auth"
1314
"github.com/git-ecosystem/git-bundle-server/internal/log"
1415
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
1516
)
@@ -27,6 +28,8 @@ func parseAuthConfig(configPath string) (auth.AuthMiddleware, error) {
2728
}
2829

2930
switch strings.ToLower(config.AuthMode) {
31+
case "fixed":
32+
return auth_internal.NewFixedCredentialAuth(config.Parameters)
3033
default:
3134
return nil, fmt.Errorf("unrecognized auth mode '%s'", config.AuthMode)
3235
}

docs/man/git-bundle-web-server.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,31 @@ The auth config JSON contains the following fields:
4040

4141
*mode* (string)::
4242
The auth mode to use. Not case-sensitive.
43+
+
44+
Available options:
45+
46+
- _fixed_
4347

4448
*parameters* (object)::
4549
A structure containing mode-specific key-value configuration fields, if
4650
applicable. May be optional, depending on *mode*.
4751

52+
=== Examples
53+
54+
Static, server-wide username & password ("admin" & "bundle_server",
55+
respectively):
56+
57+
[source,json]
58+
----
59+
{
60+
"mode": "fixed",
61+
"parameters": {
62+
"username": "admin",
63+
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
64+
}
65+
}
66+
----
67+
4868
== SEE ALSO
4969

5070
man:git-bundle-server[1], man:git-bundle[1], man:git-fetch[1]

docs/technical/auth-config.md

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ The JSON file contains the following fields:
2222
<td><code>mode</code></td>
2323
<td>string</td>
2424
<td>
25-
The auth mode to use. Not case-sensitive.
25+
<p>The auth mode to use. Not case-sensitive.</p>
26+
Available options:
27+
<ul>
28+
<li><code>fixed</code></li>
29+
</ul>
2630
</td>
2731
</tr>
2832
<tr>
@@ -35,3 +39,103 @@ The JSON file contains the following fields:
3539
</tr>
3640
</tbody>
3741
</table>
42+
43+
## Built-in modes
44+
45+
### Fixed/single-user auth (server-wide)
46+
47+
**Mode: `fixed`**
48+
49+
This mode implements [Basic authentication][basic-rfc], authenticating each
50+
request against a fixed username/password pair that is global to the web server.
51+
52+
[basic-rfc]: https://datatracker.ietf.org/doc/html/rfc7617
53+
54+
#### Parameters
55+
56+
The `parameters` object _must_ be specified for this mode, and both of the
57+
fields below are required.
58+
59+
<table>
60+
<thead>
61+
<tr>
62+
<th>Field</th>
63+
<th>Type</th>
64+
<th>Description</th>
65+
</tr>
66+
</thead>
67+
<tbody>
68+
<tr>
69+
<td><code>username</code></td>
70+
<td>string</td>
71+
<td>
72+
The username string for authentication. The username <i>must
73+
not</i> contain a colon (":").
74+
</td>
75+
</tr>
76+
<tr>
77+
<td><code>passwordHash</code></td>
78+
<td>string</td>
79+
<td>
80+
<p>
81+
The SHA256 hash of the password string. There are no
82+
restrictions on characters used for the password.
83+
</p>
84+
<p>
85+
The hash of a string can be generated on the command line
86+
with the command
87+
<code>echo -n '&lt;your string&gt;' | shasum -a 256</code>.
88+
</p>
89+
</td>
90+
</tr>
91+
</tbody>
92+
</table>
93+
94+
#### Examples
95+
96+
Valid (username `admin`, password `test`):
97+
98+
```json
99+
{
100+
"mode": "fixed",
101+
"parameters": {
102+
"username": "admin",
103+
"passwordHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
104+
}
105+
}
106+
```
107+
108+
Valid (empty username & password):
109+
110+
```json
111+
{
112+
"mode": "fixed",
113+
"parameters": {
114+
"usernameHash": "",
115+
"passwordHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
116+
}
117+
}
118+
```
119+
120+
Invalid:
121+
122+
```json
123+
{
124+
"mode": "fixed",
125+
"parameters": {
126+
"username": "admin",
127+
"passwordHash": "test123"
128+
}
129+
}
130+
```
131+
132+
Invalid:
133+
134+
```json
135+
{
136+
"mode": "fixed",
137+
"parameters": {
138+
"username": "admin:MY_PASSWORD",
139+
}
140+
}
141+
```

examples/auth/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Auth configuration examples
2+
3+
This directory contains examples of auth configurations that may be used as a
4+
reference for setting up auth for a bundle server.
5+
6+
> **Warning**
7+
>
8+
> The examples contained within this directory should not be used directly in a
9+
> production context due to publicly-visible (in this repo) credentials.
10+
11+
## Built-in modes
12+
13+
### Fixed credential/single-user auth
14+
15+
The file [`config/fixed.json`][fixed-config] configures [Basic
16+
authentication][basic] with username "admin" and password "bundle_server".
17+
18+
[fixed-config]: ./config/fixed.json
19+
[basic]: ../../docs/technical/auth-config.md#basic-auth-server-wide

examples/auth/config/fixed.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"mode": "fixed",
3+
"parameters": {
4+
"username": "admin",
5+
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
6+
}
7+
}

internal/auth/middleware.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package auth
2+
3+
import (
4+
"crypto/sha256"
5+
"crypto/subtle"
6+
"encoding/hex"
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
13+
)
14+
15+
/* Built-in auth modes */
16+
// Authorize users with credentials matching a static username/password pair
17+
// that applies to the whole server.
18+
type fixedCredentialAuth struct {
19+
usernameHash [32]byte
20+
passwordHash [32]byte
21+
}
22+
23+
type fixedCredentialAuthParams struct {
24+
Username string `json:"username"`
25+
PasswordHash string `json:"passwordHash"`
26+
}
27+
28+
func NewFixedCredentialAuth(rawParameters json.RawMessage) (auth.AuthMiddleware, error) {
29+
if len(rawParameters) == 0 {
30+
return nil, fmt.Errorf("parameters JSON must exist")
31+
}
32+
33+
var params fixedCredentialAuthParams
34+
err := json.Unmarshal(rawParameters, &params)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
// Check for invalid username characters
40+
if strings.Contains(params.Username, ":") {
41+
return nil, fmt.Errorf("username contains a colon (\":\")")
42+
}
43+
44+
// Make sure password hash is a valid hash
45+
passwordHashBytes, err := hex.DecodeString(params.PasswordHash)
46+
if err != nil {
47+
return nil, fmt.Errorf("passwordHash is invalid: %w", err)
48+
} else if len(passwordHashBytes) != 32 {
49+
return nil, fmt.Errorf("passwordHash is incorrect length (%d vs. expected 32)", len(passwordHashBytes))
50+
}
51+
52+
return &fixedCredentialAuth{
53+
usernameHash: sha256.Sum256([]byte(params.Username)),
54+
passwordHash: [32]byte(passwordHashBytes),
55+
}, nil
56+
}
57+
58+
func (a *fixedCredentialAuth) Authorize(r *http.Request, _ string, _ string) auth.AuthResult {
59+
username, password, ok := r.BasicAuth()
60+
if ok {
61+
usernameHash := sha256.Sum256([]byte(username))
62+
passwordHash := sha256.Sum256([]byte(password))
63+
64+
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], a.usernameHash[:]) == 1)
65+
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], a.passwordHash[:]) == 1)
66+
67+
if usernameMatch && passwordMatch {
68+
return auth.Allow()
69+
} else {
70+
// Return a 404 status even though the issue is that the user is
71+
// forbidden so we don't indirectly reveal which repositories are
72+
// configured in the bundle server.
73+
return auth.Deny(404)
74+
}
75+
}
76+
77+
return auth.Deny(401, auth.Header{Key: "WWW-Authenticate", Value: `Basic realm="restricted", charset="UTF-8"`})
78+
}

0 commit comments

Comments
 (0)