chrly/auth/jwt.go
ErickSkrauch f120064fe3
Implemented API endpoint to update skin information
Added tests to jwt package
Reworked redis backend implementation
Skin repository now have methods to remove skins by user id or username
2018-01-23 18:43:37 +03:00

173 lines
3.2 KiB
Go

package auth
import (
"encoding/base64"
"math"
"math/rand"
"net/http"
"os"
"strings"
"time"
"github.com/SermoDigital/jose/crypto"
"github.com/SermoDigital/jose/jws"
"github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
)
var fs = afero.NewOsFs()
var hashAlg = crypto.SigningMethodHS256
const appHomeDirName = ".minecraft-skinsystem"
const scopesClaim = "scopes"
type Scope string
var (
SkinScope = Scope("skin")
)
type JwtAuth struct {
signingKey []byte
}
func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) {
key, err := t.getSigningKey()
if err != nil {
return nil, err
}
claims := jws.Claims{}
claims.Set(scopesClaim, scopes)
claims.SetIssuedAt(time.Now())
encoder := jws.NewJWT(claims, hashAlg)
token, err := encoder.Serialize(key)
if err != nil {
return nil, err
}
return token, nil
}
func (t *JwtAuth) GenerateSigningKey() error {
if err := createAppHomeDir(); err != nil {
return err
}
key := generateRandomBytes(64)
if err := afero.WriteFile(fs, getKeyPath(), key, 0600); err != nil {
return err
}
return nil
}
func (t *JwtAuth) Check(req *http.Request) error {
bearerToken := req.Header.Get("Authorization")
if bearerToken == "" {
return &Unauthorized{"Authentication header not presented"}
}
if !strings.EqualFold(bearerToken[0:7], "BEARER ") {
return &Unauthorized{"Cannot recognize JWT token in passed value"}
}
tokenStr := bearerToken[7:]
token, err := jws.ParseJWT([]byte(tokenStr))
if err != nil {
return &Unauthorized{"Cannot parse passed JWT token"}
}
signKey, err := t.getSigningKey()
if err != nil {
return err
}
err = token.Validate(signKey, hashAlg)
if err != nil {
return &Unauthorized{"JWT token have invalid signature. It corrupted or expired."}
}
return nil
}
func (t *JwtAuth) getSigningKey() ([]byte, error) {
if t.signingKey == nil {
path := getKeyPath()
if _, err := fs.Stat(path); err != nil {
if os.IsNotExist(err) {
return nil, &SigningKeyNotAvailable{}
}
return nil, err
}
key, err := afero.ReadFile(fs, path)
if err != nil {
return nil, err
}
t.signingKey = key
}
return t.signingKey, nil
}
func createAppHomeDir() error {
path := getAppHomeDirPath()
if _, err := fs.Stat(path); os.IsNotExist(err) {
err := fs.Mkdir(path, 0755) // rwx r-x r-x
if err != nil {
return err
}
}
return nil
}
func getAppHomeDirPath() string {
path, err := homedir.Expand("~/" + appHomeDirName)
if err != nil {
panic(err)
}
return path
}
func getKeyPath() string {
return getAppHomeDirPath() + "/jwt-key"
}
func generateRandomBytes(n int) []byte {
// base64 will increase length in 1.37 times
// +1 is needed to ensure, that after base64 we will do not have any '===' characters
randLen := int(math.Ceil(float64(n) / 1.37)) + 1
randBytes := make([]byte, randLen)
rand.Read(randBytes)
// +5 is needed to have additional buffer for the next set of XX=== characters
resBytes := make([]byte, n + 5)
base64.URLEncoding.Encode(resBytes, randBytes)
return resBytes[:n]
}
type Unauthorized struct {
Reason string
}
func (e *Unauthorized) Error() string {
if e.Reason != "" {
return e.Reason
}
return "Unauthorized"
}
type SigningKeyNotAvailable struct {
}
func (*SigningKeyNotAvailable) Error() string {
return "Signing key not available"
}