mirror of
https://github.com/pushbits/server.git
synced 2025-07-29 14:28:01 +02:00
Initialize repository
This commit is contained in:
commit
1d758fcfd0
28 changed files with 1107 additions and 0 deletions
106
authentication/authentication.go
Normal file
106
authentication/authentication.go
Normal 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
30
authentication/context.go
Normal 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
|
||||
}
|
21
authentication/credentials/credentials.go
Normal file
21
authentication/credentials/credentials.go
Normal 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
54
authentication/token.go
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue