Skip to content

Commit

Permalink
feat(authn): Basic Autentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
ncarlier committed May 27, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ca1adbe commit 5dcd1ff
Showing 9 changed files with 158 additions and 31 deletions.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -21,27 +21,33 @@ Read your Internet article flow in one place with complete peace of mind and fre

## Installation

Run the following command:
Using Go compiler:

```bash
$ go install -v github.com/ncarlier/readflow@latest
go install -v github.com/ncarlier/readflow@latest
```

**Or** download the binary regarding your architecture:
**Or** using pre-compiled binary:

```bash
$ curl -sf https://gobinaries.com/ncarlier/readflow | sh
$ # or
$ curl -s https://raw.githubusercontent.com/ncarlier/readflow/master/install.sh | bash
curl -sf https://gobinaries.com/ncarlier/readflow | sh
# or
curl -s https://raw.githubusercontent.com/ncarlier/readflow/master/install.sh | bash
```

**Or** use Docker:
**Or** using Docker:

```bash
$ docker run -it --rm \
-p 8080:8080 \
-e READFLOW_DB=<YOUR POSTGERSQL CONNECTION STRING> \
ncarlier/readflow:edge
docker run -it --rm \
-p 8080:8080 \
-e READFLOW_DB=<YOUR POSTGERSQL CONNECTION STRING> \
ncarlier/readflow:edge
```

**Or** using Docker Compose:

```bash
docker compose up
```

## Configuration
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -43,7 +43,9 @@ services:
- READFLOW_DB=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-secret}@db/${POSTGRES_DB:-readflow}?sslmode=disable
- READFLOW_LISTEN_METRICS=:9090
- READFLOW_IMAGE_PROXY_URL=http://imaginary:9000
- READFLOW_AUTHN=mock
- READFLOW_AUTHN=file:///var/local/demo.htpasswd
volumes:
- ${PWD}/var/demo.htpasswd:/var/local/demo.htpasswd

networks:
default:
16 changes: 8 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
@@ -74,20 +74,20 @@ func main() {
// Configure the DB
database, err := db.NewDB(conf.Global.DatabaseURI)
if err != nil {
log.Fatal().Err(err).Msg("could not configure database")
log.Fatal().Err(err).Msg("unable to configure the database")
}

// Configure download cache
downloadCache, err := cache.NewDefault("readflow-downloads")
if err != nil {
log.Fatal().Err(err).Msg("could not configure cache")
log.Fatal().Err(err).Msg("unable to configure the cache storage")
}

// Configure the service registry
err = service.Configure(*conf, database, downloadCache)
if err != nil {
database.Close()
log.Fatal().Err(err).Msg("could not init service registry")
log.Fatal().Err(err).Msg("unable to configure the service registry")
}

// Start job scheduler
@@ -108,7 +108,7 @@ func main() {
go func() {
log.Info().Str("listen", conf.Global.MetricsListenAddr).Msg("metrics server started")
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Str("listen", conf.Global.MetricsListenAddr).Msg("could not start metrics server")
log.Fatal().Err(err).Str("listen", conf.Global.MetricsListenAddr).Msg("unable to start the metrics server")
}
}()
}
@@ -128,17 +128,17 @@ func main() {

server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("could not gracefully shutdown the server")
log.Fatal().Err(err).Msg("unable to gracefully shutdown the server")
}
if metricsServer != nil {
metric.StopCollectors()
if err := metricsServer.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("could not gracefully shutdown metrics server")
log.Fatal().Err(err).Msg("unable to gracefully shutdown the metrics server")
}
}

if err := downloadCache.Close(); err != nil {
log.Error().Err(err).Msg("could not gracefully shutdown cache provider")
log.Error().Err(err).Msg("unable to gracefully shutdown the cache storage")
}

if err := database.Close(); err != nil {
@@ -153,7 +153,7 @@ func main() {
log.Info().Str("listen", conf.Global.ListenAddr).Msg("server started")

if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Str("listen", conf.Global.ListenAddr).Msg("could not start the server")
log.Fatal().Err(err).Str("listen", conf.Global.ListenAddr).Msg("unable to start the server")
}

<-done
72 changes: 72 additions & 0 deletions pkg/helper/htpasswd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package helper

import (
"crypto/sha1"
"encoding/base64"
"encoding/csv"
"regexp"

"golang.org/x/crypto/bcrypt"
)

var (
shaRe = regexp.MustCompile(`^{SHA}`)
bcrRe = regexp.MustCompile(`^\$2b\$|^\$2a\$|^\$2y\$`)
)

// HtpasswdFile is a map for usernames to passwords.
type HtpasswdFile struct {
location string
users map[string]string
}

// newHtpasswdFromFile reads the users and passwords from a htpasswd file and returns them.
func NewHtpasswdFromFile(location string) (*HtpasswdFile, error) {
r, err := OpenResource(location)
if err != nil {
return nil, err
}
defer r.Close()

cr := csv.NewReader(r)
cr.Comma = ':'
cr.Comment = '#'
cr.TrimLeadingSpace = true

records, err := cr.ReadAll()
if err != nil {
return nil, err
}

users := make(map[string]string)
for _, record := range records {
users[record[0]] = record[1]
}

return &HtpasswdFile{
location: location,
users: users,
}, nil
}

func (h *HtpasswdFile) Authenticate(username string, password string) bool {
pwd, exists := h.users[username]
if !exists {
return false
}

switch {
case shaRe.MatchString(pwd):
d := sha1.New()
_, _ = d.Write([]byte(password))
if pwd[5:] == base64.StdEncoding.EncodeToString(d.Sum(nil)) {
return true
}
case bcrRe.MatchString(pwd):
err := bcrypt.CompareHashAndPassword([]byte(pwd), []byte(password))
if err == nil {
return true
}
}
return false
}
24 changes: 15 additions & 9 deletions pkg/middleware/auth.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
package middleware

import (
"fmt"
"strings"

"github.com/rs/zerolog/log"
)

const tpl = "using %s as authentication backend"
const usingAuthNMsg = "using authentication"

// Auth is a middleware to authenticate HTTP request
func Auth(method string) Middleware {
switch method {
case "mock":
log.Info().Msg(fmt.Sprintf(tpl, "Mock"))
switch {
case method == "mock":
log.Info().Str("method", method).Msg(usingAuthNMsg)
return MockAuth
case "proxy":
log.Info().Msg(fmt.Sprintf(tpl, "Proxy"))
case method == "proxy":
log.Info().Str("method", method).Msg(usingAuthNMsg)
return ProxyAuth
default:
log.Info().Str("authority", method).Msg(fmt.Sprintf(tpl, "OpenID Connect"))
case strings.HasPrefix(method, "file://"):
log.Info().Str("method", "basic").Str("htpasswd", method).Msg(usingAuthNMsg)
return BasicAuth(method)
case strings.HasPrefix(method, "https://"):
log.Info().Str("method", "bearer").Str("authority", method).Msg(usingAuthNMsg)
return OpenIDConnectJWTAuth(method)
default:
log.Fatal().Str("method", method).Msg("non supported authentication method")
return nil
}
}
40 changes: 40 additions & 0 deletions pkg/middleware/basic-auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package middleware

import (
"context"
"net/http"

"github.com/ncarlier/readflow/pkg/constant"
"github.com/ncarlier/readflow/pkg/helper"
"github.com/ncarlier/readflow/pkg/service"
"github.com/rs/zerolog/log"
)

// BasicAuth is a middleware to checks HTTP request credentials from Basic AuthN method
func BasicAuth(location string) Middleware {
htpasswd, err := helper.NewHtpasswdFromFile(location)
if err != nil {
log.Fatal().Err(err).Str("location", location).Msg("unable to read htpasswd file")
}

return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
username, password, ok := r.BasicAuth()
if ok && htpasswd.Authenticate(username, password) {
user, err := service.Lookup().GetOrRegisterUser(ctx, username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx = context.WithValue(ctx, constant.ContextUser, *user)
ctx = context.WithValue(ctx, constant.ContextUserID, *user.ID)
ctx = context.WithValue(ctx, constant.ContextIsAdmin, false)
inner.ServeHTTP(w, r.WithContext(ctx))
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="readflow", charset="UTF-8"`)
jsonErrors(w, "Unauthorized", 401)
})
}
}
2 changes: 1 addition & 1 deletion pkg/middleware/oidc-jwt-auth.go
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ func OpenIDConnectJWTAuth(authority string) Middleware {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

w.Header().Set("WWW-Authenticate", `Bearer realm="Restricted"`)
w.Header().Set("WWW-Authenticate", `Bearer realm="readflow"`)

token, err := jwtRequest.ParseFromRequest(r, jwtRequest.OAuth2Extractor, func(token *jwt.Token) (i interface{}, e error) {
if id, ok := token.Header["kid"]; ok {
2 changes: 1 addition & 1 deletion pkg/middleware/proxy-auth.go
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ func ProxyAuth(inner http.Handler) http.Handler {
inner.ServeHTTP(w, r.WithContext(ctx))
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="Ah ah ah, you didn't say the magic word"`)
w.Header().Set("Proxy-Authenticate", `Basic realm="readflow"`)
jsonErrors(w, "Unauthorized", 401)
})
}
1 change: 1 addition & 0 deletions var/demo.htpasswd
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
demo:$2y$05$pyVCV7lwL1Scis6Lz.KyZuS9..KCD2y7dhKBkEzXlR9RH3VVNqdLG

0 comments on commit 5dcd1ff

Please sign in to comment.