Initialize repository

This commit is contained in:
eikendev 2020-07-26 00:28:38 +02:00
commit 1d758fcfd0
No known key found for this signature in database
GPG key ID: A1BDB1B28C8EF694
28 changed files with 1107 additions and 0 deletions

View file

@ -0,0 +1,106 @@
package authentication
import (
"errors"
"net/http"
"github.com/eikendev/pushbits/authentication/credentials"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
const (
headerName = "X-Gotify-Key"
)
// The Database interface for encapsulating database access.
type Database interface {
GetApplicationByToken(token string) (*model.Application, error)
GetUserByName(name string) (*model.User, error)
}
// Authenticator is the provider for authentication middleware.
type Authenticator struct {
DB Database
}
type hasUserProperty func(user *model.User) bool
func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) {
if name, password, ok := ctx.Request.BasicAuth(); ok {
if user, err := a.DB.GetUserByName(name); err != nil {
return nil, err
} else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) {
return user, nil
} else {
return nil, errors.New("credentials were invalid")
}
}
return nil, errors.New("no credentials were supplied")
}
func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc {
return func(ctx *gin.Context) {
user, err := a.userFromBasicAuth(ctx)
if err != nil {
ctx.AbortWithError(http.StatusForbidden, err)
return
}
if !has(user) {
ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed"))
return
}
ctx.Set("user", user)
}
}
// RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request.
func (a *Authenticator) RequireUser() gin.HandlerFunc {
return a.requireUserProperty(func(user *model.User) bool {
return true
})
}
// RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request.
func (a *Authenticator) RequireAdmin() gin.HandlerFunc {
return a.requireUserProperty(func(user *model.User) bool {
return user.IsAdmin
})
}
func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string {
if token := a.tokenFromQuery(ctx); token != "" {
return token
} else if token := a.tokenFromHeader(ctx); token != "" {
return token
}
return ""
}
func (a *Authenticator) tokenFromQuery(ctx *gin.Context) string {
return ctx.Request.URL.Query().Get("token")
}
func (a *Authenticator) tokenFromHeader(ctx *gin.Context) string {
return ctx.Request.Header.Get(headerName)
}
// RequireApplicationToken returns a Gin middleware which requires an application token to be supplied with the request.
func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := a.tokenFromQueryOrHeader(ctx)
app, err := a.DB.GetApplicationByToken(token)
if err != nil {
ctx.AbortWithError(http.StatusForbidden, err)
return
}
ctx.Set("app", app)
}
}

30
authentication/context.go Normal file
View file

@ -0,0 +1,30 @@
package authentication
import (
"errors"
"net/http"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
// GetApplication returns the application which was previously registered by the authentication middleware.
func GetApplication(ctx *gin.Context) *model.Application {
app, ok := ctx.MustGet("app").(*model.Application)
if app == nil || !ok {
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving application from context"))
}
return app
}
// GetUser returns the user which was previously registered by the authentication middleware.
func GetUser(ctx *gin.Context) *model.User {
user, ok := ctx.MustGet("user").(*model.User)
if user == nil || !ok {
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving user from context"))
}
return user
}

View file

@ -0,0 +1,21 @@
package credentials
import "golang.org/x/crypto/bcrypt"
// CreatePassword returns a hashed version of the given password.
// TODO: Make strength configurable.
func CreatePassword(pw string) []byte {
strength := 12
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pw), strength)
if err != nil {
panic(err)
}
return hashedPassword
}
// ComparePassword compares a hashed password with its possible plaintext equivalent.
func ComparePassword(hashedPassword, password []byte) bool {
return bcrypt.CompareHashAndPassword(hashedPassword, password) == nil
}

54
authentication/token.go Normal file
View file

@ -0,0 +1,54 @@
package authentication
import (
"crypto/rand"
"math/big"
)
var (
tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
randomTokenLength = 64
applicationPrefix = "A"
)
func randIntn(n int) int {
max := big.NewInt(int64(n))
res, err := rand.Int(rand.Reader, max)
if err != nil {
panic("random source is not available")
}
return int(res.Int64())
}
// GenerateNotExistingToken receives a token generation function and a function to check whether the token exists, returns a unique token.
func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
for {
token := generateToken()
if !tokenExists(token) {
return token
}
}
}
func generateRandomString(length int) string {
res := make([]byte, length)
for i := range res {
index := randIntn(len(tokenCharacters))
res[i] = tokenCharacters[index]
}
return string(res)
}
func generateRandomToken(prefix string) string {
return prefix + generateRandomString(randomTokenLength)
}
// GenerateApplicationToken generates a token for an application.
func GenerateApplicationToken() string {
return generateRandomToken(applicationPrefix)
}