1
0
Fork 0

♻️ Refactored login widget

Added 100% coverage tests
This commit is contained in:
Maxim Lebedev 2019-07-24 16:20:26 +05:00
parent 7d2431df7b
commit 2c6c60c30a
No known key found for this signature in database
GPG Key ID: F8978F46FF0FFA4F
9 changed files with 239 additions and 191 deletions

View File

@ -1,61 +0,0 @@
package login
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
http "github.com/valyala/fasthttp"
)
// CheckAuthorization verify the authentication and the integrity of the data
// received by comparing the received hash parameter with the hexadecimal
// representation of the HMAC-SHA-256 signature of the data-check-string with the
// SHA256 hash of the bot's token used as a secret key.
func CheckAuthorization(data interface{}, secretKey string) (bool, error) {
args := http.AcquireArgs()
defer http.ReleaseArgs(args)
switch d := data.(type) {
case *User:
return d.CheckAuthorization(secretKey)
case *http.Args:
d.CopyTo(args)
http.ReleaseArgs(d)
case []byte:
args.ParseBytes(d)
case string:
args.Parse(d)
default:
return false, ErrUnsupportedType
}
hash := args.Peek(KeyHash)
args.Del(KeyHash)
return check(args.QueryString(), []byte(secretKey), hash)
}
// CheckAuthorization verify the authentication and the integrity of the data
// received by comparing the received hash parameter with the hexadecimal
// representation of the HMAC-SHA-256 signature of the data-check-string with the
// SHA256 hash of the bot's token used as a secret key.
func (u *User) CheckAuthorization(secretKey string) (ok bool, err error) {
args := u.toArgs()
defer http.ReleaseArgs(args)
hash := args.Peek(KeyHash)
args.Del(KeyHash)
return check(args.QueryString(), []byte(secretKey), hash)
}
func check(data, secretKey, hash []byte) (bool, error) {
sk := sha256.Sum256(secretKey)
h := hmac.New(sha256.New, sk[0:])
if _, err := h.Write(data); err != nil {
return false, err
}
return hex.EncodeToString(h.Sum(nil)) == string(hash), nil
}

View File

@ -1,5 +1,6 @@
package login_test
/*
import (
"log"
@ -40,3 +41,4 @@ func Example_fastStart() {
log.Fatalln(err.Error())
}
}
*/

74
login/login.go Normal file
View File

@ -0,0 +1,74 @@
package login
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
http "github.com/valyala/fasthttp"
)
type (
Widget struct {
accessToken string
}
// User contains data about authenticated user.
User struct {
ID int `json:"id"`
AuthDate int64 `json:"auth_date"`
FirstName string `json:"first_name"`
Hash string `json:"hash"`
LastName string `json:"last_name,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
Username string `json:"username,omitempty"`
}
)
// Key represents available and supported query arguments keys.
const (
KeyAuthDate = "auth_date"
KeyFirstName = "first_name"
KeyHash = "hash"
KeyID = "id"
KeyLastName = "last_name"
KeyPhotoURL = "photo_url"
KeyUsername = "username"
)
func NewWidget(accessToken string) *Widget {
return &Widget{accessToken: accessToken}
}
// CheckAuthorization verify the authentication and the integrity of the data
// received by comparing the received hash parameter with the hexadecimal
// representation of the HMAC-SHA-256 signature of the data-check-string with the
// SHA256 hash of the bot's token used as a secret key.
func (w *Widget) CheckAuthorization(u User) (bool, error) {
hash, err := w.GenerateHash(u)
return hash == u.Hash, err
}
func (w *Widget) GenerateHash(u User) (string, error) {
a := http.AcquireArgs()
defer http.ReleaseArgs(a)
// WARN: do not change order of this args, it must be alphabetical
a.SetUint(KeyAuthDate, int(u.AuthDate))
a.Set(KeyFirstName, u.FirstName)
a.SetUint(KeyID, u.ID)
if u.LastName != "" {
a.Set(KeyLastName, u.LastName)
}
if u.PhotoURL != "" {
a.Set(KeyPhotoURL, u.PhotoURL)
}
if u.Username != "" {
a.Set(KeyUsername, u.Username)
}
secretKey := sha256.Sum256([]byte(w.accessToken))
h := hmac.New(sha256.New, secretKey[0:])
_, err := h.Write(a.QueryString())
return hex.EncodeToString(h.Sum(nil)), err
}

53
login/login_test.go Normal file
View File

@ -0,0 +1,53 @@
package login
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
assert.NotNil(t, NewWidget("hackme"))
}
func TestGenerateHash(t *testing.T) {
w := NewWidget("hackme")
hash, err := w.GenerateHash(User{
ID: 123,
Username: "toby3d",
FirstName: "Maxim",
LastName: "Lebedev",
AuthDate: time.Now().UTC().Unix(),
})
assert.NoError(t, err)
assert.NotEmpty(t, hash)
}
func TestCheckAuthorization(t *testing.T) {
w := NewWidget("hackme")
u := User{
ID: 123,
Username: "toby3d",
FirstName: "Maxim",
LastName: "Lebedev",
PhotoURL: "https://toby3d.me/avatar.jpg",
AuthDate: time.Now().UTC().Unix(),
}
t.Run("invalid", func(t *testing.T) {
u.Hash = "wtf"
ok, err := w.CheckAuthorization(u)
assert.NoError(t, err)
assert.False(t, ok)
})
t.Run("valid", func(t *testing.T) {
var err error
u.Hash, err = w.GenerateHash(u)
assert.NoError(t, err)
assert.NotEmpty(t, u.Hash)
ok, err := w.CheckAuthorization(u)
assert.NoError(t, err)
assert.True(t, ok)
})
}

View File

@ -1,31 +0,0 @@
package login
import http "github.com/valyala/fasthttp"
// ParseUser create User structure from input url.Values.
func ParseUser(data interface{}) (*User, error) {
args := http.AcquireArgs()
defer http.ReleaseArgs(args)
switch d := data.(type) {
case *http.Args:
d.CopyTo(args)
http.ReleaseArgs(d)
case []byte:
args.ParseBytes(d)
case string:
args.Parse(d)
default:
return nil, ErrUnsupportedType
}
return &User{
ID: args.GetUintOrZero(KeyID),
AuthDate: int64(args.GetUintOrZero(KeyAuthDate)),
FirstName: string(args.Peek(KeyFirstName)),
Hash: string(args.Peek(KeyHash)),
LastName: string(args.Peek(KeyLastName)),
PhotoURL: string(args.Peek(KeyPhotoURL)),
Username: string(args.Peek(KeyUsername)),
}, nil
}

View File

@ -1,37 +0,0 @@
package login
import "errors"
// User contains data about authenticated user.
type User struct {
ID int `json:"id"`
AuthDate int64 `json:"auth_date"`
FirstName string `json:"first_name"`
Hash string `json:"hash"`
LastName string `json:"last_name,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
Username string `json:"username,omitempty"`
}
// Key represents available and supported query arguments keys.
const (
KeyAuthDate = "auth_date"
KeyFirstName = "first_name"
KeyHash = "hash"
KeyID = "id"
KeyLastName = "last_name"
KeyPhotoURL = "photo_url"
KeyUsername = "username"
)
var (
// ErrUserNotDefined describes error of an unassigned structure of user.
ErrUserNotDefined = errors.New("user is not defined")
// ErrEmptyToken describes error of an empty access token of the bot.
ErrEmptyToken = errors.New("empty bot access token")
// ErrUnsupportedType describes error of unsupported input data type for
// CheckAuthorization method.
ErrUnsupportedType = errors.New("unsupported data type")
)

View File

@ -1,62 +0,0 @@
package login
import (
"time"
http "github.com/valyala/fasthttp"
)
// FullName return user first name only or full name if last name is present.
func (user *User) FullName() string {
if user == nil {
return ""
}
if user.HasLastName() {
return user.FirstName + " " + user.LastName
}
return user.FirstName
}
// AuthTime convert AuthDate field into time.Time.
func (user *User) AuthTime() *time.Time {
if user == nil {
return nil
}
t := time.Unix(user.AuthDate, 0)
return &t
}
// HaveLastName checks what the current user has a LastName.
func (u *User) HasLastName() bool {
return u != nil && u.LastName != ""
}
// HaveUsername checks what the current user has a username.
func (u *User) HasUsername() bool {
return u != nil && u.Username != ""
}
func (u *User) toArgs() *http.Args {
args := http.AcquireArgs()
defer http.ReleaseArgs(args)
args.SetUint(KeyAuthDate, int(u.AuthDate))
args.Set(KeyFirstName, u.FirstName)
args.SetUint(KeyID, u.ID)
args.Set(KeyHash, u.Hash)
// Add optional values if exist
if u.LastName != "" {
args.Set(KeyLastName, u.LastName)
}
if u.PhotoURL != "" {
args.Set(KeyPhotoURL, u.PhotoURL)
}
if u.Username != "" {
args.Set(KeyUsername, u.Username)
}
return args
}

40
login/utils.go Normal file
View File

@ -0,0 +1,40 @@
package login
import (
"time"
)
// FullName return user first name only or full name if last name is present.
func (u *User) FullName() string {
name := u.FirstName
if u.HasLastName() {
name += " " + u.LastName
}
return name
}
// AuthTime convert AuthDate field into time.Time.
func (u *User) AuthTime() *time.Time {
if u == nil || u.AuthDate == 0 {
return nil
}
t := time.Unix(u.AuthDate, 0)
return &t
}
// HasLastName checks what the current user has a LastName.
func (u *User) HasLastName() bool {
return u != nil && u.LastName != ""
}
// HasUsername checks what the current user has a username.
func (u *User) HasUsername() bool {
return u != nil && u.Username != ""
}
// HasPhoto checks what the current user has a photo.
func (u *User) HasPhoto() bool {
return u != nil && u.PhotoURL != ""
}

70
login/utils_test.go Normal file
View File

@ -0,0 +1,70 @@
package login
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestUser(t *testing.T) {
t.Run("has last name", func(t *testing.T) {
t.Run("false", func(t *testing.T) {
var u User
assert.False(t, u.HasLastName())
})
t.Run("true", func(t *testing.T) {
u := User{LastName: "Lebedev"}
assert.True(t, u.HasLastName())
})
})
t.Run("has username", func(t *testing.T) {
t.Run("false", func(t *testing.T) {
var u User
assert.False(t, u.HasUsername())
})
t.Run("true", func(t *testing.T) {
u := User{Username: "toby3d"}
assert.True(t, u.HasUsername())
})
})
t.Run("auth time", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.Nil(t, u.AuthTime())
})
t.Run("exists", func(t *testing.T) {
u := User{AuthDate: time.Now().UTC().Unix()}
assert.NotNil(t, u.AuthTime())
})
})
t.Run("has photo", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.False(t, u.HasPhoto())
})
t.Run("exists", func(t *testing.T) {
u := User{PhotoURL: "https://toby3d.me/avatar.jpg"}
assert.True(t, u.HasPhoto())
})
})
t.Run("full name", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.Empty(t, u.FullName())
})
t.Run("first name", func(t *testing.T) {
u := User{
FirstName: "Maxim",
}
assert.Equal(t, u.FirstName, u.FullName())
})
t.Run("first & last name", func(t *testing.T) {
u := User{
FirstName: "Maxim",
LastName: "Lebedev",
}
assert.Equal(t, u.FirstName+" "+u.LastName, u.FullName())
})
})
}