Skip to content

Commit

Permalink
bundle-server: implement simple user/pass auth middleware
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
vdye committed May 11, 2023
1 parent 6426168 commit 7a17362
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 1 deletion.
3 changes: 3 additions & 0 deletions cmd/git-bundle-web-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/git-ecosystem/git-bundle-server/cmd/utils"
"github.com/git-ecosystem/git-bundle-server/internal/argparse"
auth_internal "github.com/git-ecosystem/git-bundle-server/internal/auth"
"github.com/git-ecosystem/git-bundle-server/internal/log"
"github.com/git-ecosystem/git-bundle-server/pkg/auth"
)
Expand All @@ -27,6 +28,8 @@ func parseAuthConfig(configPath string) (auth.AuthMiddleware, error) {
}

switch strings.ToLower(config.AuthMode) {
case "fixed":
return auth_internal.NewFixedCredentialAuth(config.Parameters)
default:
return nil, fmt.Errorf("unrecognized auth mode '%s'", config.AuthMode)
}
Expand Down
20 changes: 20 additions & 0 deletions docs/man/git-bundle-web-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,31 @@ The auth config JSON contains the following fields:

*mode* (string)::
The auth mode to use. Not case-sensitive.
+
Available options:

- _fixed_

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

=== Examples

Static, server-wide username & password ("admin" & "bundle_server",
respectively):

[source,json]
----
{
"mode": "fixed",
"parameters": {
"username": "admin",
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
}
}
----

== SEE ALSO

man:git-bundle-server[1], man:git-bundle[1], man:git-fetch[1]
106 changes: 105 additions & 1 deletion docs/technical/auth-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ The JSON file contains the following fields:
<td><code>mode</code></td>
<td>string</td>
<td>
The auth mode to use. Not case-sensitive.
<p>The auth mode to use. Not case-sensitive.</p>
Available options:
<ul>
<li><code>fixed</code></li>
</ul>
</td>
</tr>
<tr>
Expand All @@ -35,3 +39,103 @@ The JSON file contains the following fields:
</tr>
</tbody>
</table>

## Built-in modes

### Fixed/single-user auth (server-wide)

**Mode: `fixed`**

This mode implements [Basic authentication][basic-rfc], authenticating each
request against a fixed username/password pair that is global to the web server.

[basic-rfc]: https://datatracker.ietf.org/doc/html/rfc7617

#### Parameters

The `parameters` object _must_ be specified for this mode, and both of the
fields below are required.

<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>username</code></td>
<td>string</td>
<td>
The username string for authentication. The username <i>must
not</i> contain a colon (":").
</td>
</tr>
<tr>
<td><code>passwordHash</code></td>
<td>string</td>
<td>
<p>
The SHA256 hash of the password string. There are no
restrictions on characters used for the password.
</p>
<p>
The hash of a string can be generated on the command line
with the command
<code>echo -n '&lt;your string&gt;' | shasum -a 256</code>.
</p>
</td>
</tr>
</tbody>
</table>

#### Examples

Valid (username `admin`, password `test`):

```json
{
"mode": "fixed",
"parameters": {
"username": "admin",
"passwordHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
```

Valid (empty username & password):

```json
{
"mode": "fixed",
"parameters": {
"usernameHash": "",
"passwordHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
}
```

Invalid:

```json
{
"mode": "fixed",
"parameters": {
"username": "admin",
"passwordHash": "test123"
}
}
```

Invalid:

```json
{
"mode": "fixed",
"parameters": {
"username": "admin:MY_PASSWORD",
}
}
```
19 changes: 19 additions & 0 deletions examples/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Auth configuration examples

This directory contains examples of auth configurations that may be used as a
reference for setting up auth for a bundle server.

> **Warning**
>
> The examples contained within this directory should not be used directly in a
> production context due to publicly-visible (in this repo) credentials.
## Built-in modes

### Fixed credential/single-user auth

The file [`config/fixed.json`][fixed-config] configures [Basic
authentication][basic] with username "admin" and password "bundle_server".

[fixed-config]: ./config/fixed.json
[basic]: ../../docs/technical/auth-config.md#basic-auth-server-wide
7 changes: 7 additions & 0 deletions examples/auth/config/fixed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"mode": "fixed",
"parameters": {
"username": "admin",
"passwordHash": "c3c3520adf2f6e25672ba55dc70bcb3dd8b4ef3341bce1a5f38c5eca6571f372"
}
}
78 changes: 78 additions & 0 deletions internal/auth/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package auth

import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/git-ecosystem/git-bundle-server/pkg/auth"
)

/* Built-in auth modes */
// Authorize users with a credentials matching a static username/password pair
// that applies to the whole server.
type fixedCredentialAuth struct {
usernameHash [32]byte
passwordHash [32]byte
}

type fixedCredentialAuthParams struct {
Username string `json:"username"`
PasswordHash string `json:"passwordHash"`
}

func NewFixedCredentialAuth(rawParameters json.RawMessage) (auth.AuthMiddleware, error) {
if len(rawParameters) == 0 {
return nil, fmt.Errorf("parameters JSON must exist")
}

var params fixedCredentialAuthParams
err := json.Unmarshal(rawParameters, &params)
if err != nil {
return nil, err
}

// Check for invalid username characters
if strings.Contains(params.Username, ":") {
return nil, fmt.Errorf("username contains a colon (\":\")")
}

// Make sure password hash is a valid hash
passwordHashBytes, err := hex.DecodeString(params.PasswordHash)
if err != nil {
return nil, fmt.Errorf("passwordHash is invalid: %w", err)
} else if len(passwordHashBytes) != 32 {
return nil, fmt.Errorf("passwordHash is incorrect length (%d vs. expected 32)", len(passwordHashBytes))
}

return &fixedCredentialAuth{
usernameHash: sha256.Sum256([]byte(params.Username)),
passwordHash: [32]byte(passwordHashBytes),
}, nil
}

func (a *fixedCredentialAuth) Authorize(r *http.Request, _ string, _ string) auth.AuthResult {
username, password, ok := r.BasicAuth()
if ok {
usernameHash := sha256.Sum256([]byte(username))
passwordHash := sha256.Sum256([]byte(password))

usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], a.usernameHash[:]) == 1)
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], a.passwordHash[:]) == 1)

if usernameMatch && passwordMatch {
return auth.Allow()
} else {
// Return a 404 status even though the issue is that the user is
// forbidden so we don't indirectly reveal which repositories are
// configured in the bundle server.
return auth.Deny(404)
}
}

return auth.Deny(401, auth.Header{Key: "WWW-Authenticate", Value: `Basic realm="restricted", charset="UTF-8"`})
}
Loading

0 comments on commit 7a17362

Please sign in to comment.