Skip to content

Commit

Permalink
auth: add ability to authenticate downloads with JWT tokens
Browse files Browse the repository at this point in the history
Signed-off-by: Sumner Evans <[email protected]>
  • Loading branch information
sumnerevans committed Feb 28, 2024
1 parent 3732a5e commit 0e129cf
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 20 deletions.
7 changes: 5 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module github.com/matrix-org/rageshake

go 1.19
go 1.22

require gopkg.in/yaml.v2 v2.2.2
require (
github.com/golang-jwt/jwt/v5 v5.2.0
gopkg.in/yaml.v2 v2.2.2
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
Expand Down
74 changes: 56 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"os"
"strings"

"github.com/golang-jwt/jwt/v5"
"gopkg.in/yaml.v2"
)

Expand All @@ -37,6 +38,8 @@ type config struct {
BugsUser string `yaml:"listings_auth_user"`
BugsPass string `yaml:"listings_auth_pass"`

BugsJWTSecret string `yaml:"listings_jwt_secret"`

// External URI to /api
APIPrefix string `yaml:"api_prefix"`

Expand All @@ -47,15 +50,59 @@ type config struct {
WebhookURL string `yaml:"webhook_url"`
}

func basicAuth(handler http.Handler, username, password, realm string) http.Handler {
const (
rageshakeIssuer = "com.beeper.rageshake"
apiServerIssuer = "com.beeper.api-server"
)

func basicAuthOrJWTAuthenticated(handler http.Handler, username, password, realm string, jwtSecret []byte) http.Handler {
if (username == "" || password == "") && len(jwtSecret) == 0 {
panic("Either username or password for basic auth must be set, or JWT secret must be set, or both")
}

unauthorized := func(w http.ResponseWriter) {
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth() // pull creds from the request

// check user and pass securely
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
if !ok && len(jwtSecret) == 0 { // if no basic auth and no JWT auth, return unauthorized
unauthorized(w)
return
} else if !ok { // if no basic auth, try to do JWT auth
token, err := jwt.ParseWithClaims(r.URL.Query().Get("tok"), &jwt.RegisteredClaims{}, func(token *jwt.Token) (any, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
log.Printf("Error parsing JWT: %v", err)
unauthorized(w)
return
}

claims, ok := token.Claims.(*jwt.RegisteredClaims)
if !token.Valid || !ok {
log.Printf("Token invalid or claims not RegisteredClaims: %v", err)
unauthorized(w)
return
} else if claims.Issuer != rageshakeIssuer && claims.Issuer != apiServerIssuer {
log.Printf("Token issuer not rageshake or API server: %s", claims.Issuer)
unauthorized(w)
return
} else if claims.Subject != r.URL.Path {
log.Printf("Token subject (%s) not the request path (%s)", claims.Subject, r.URL.Path)
unauthorized(w)
return
}

log.Printf("Valid token from %s for accessing %s", claims.Issuer, claims.Subject)
} else if subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { // check user and pass securely
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
unauthorized(w)
return
}

Expand Down Expand Up @@ -99,19 +146,10 @@ func main() {

// serve files under "bugs"
ls := &logServer{"bugs"}
fs := http.StripPrefix("/api/listing/", ls)

// set auth if env vars exist
usr := cfg.BugsUser
pass := cfg.BugsPass
if usr == "" || pass == "" {
fmt.Println("No listings_auth_user/pass configured. No authentication is running for /api/listing")
} else {
fs = basicAuth(fs, usr, pass, "Riot bug reports")
}
http.Handle("/api/listing/", fs)
fs := basicAuthOrJWTAuthenticated(ls, cfg.BugsUser, cfg.BugsPass, "Riot bug reports", []byte(cfg.BugsJWTSecret))
http.Handle("/api/listing/", http.StripPrefix("/api/listing/", fs))

http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "ok")
})

Expand Down

0 comments on commit 0e129cf

Please sign in to comment.