1
0
Fork 0

♻️ Refactored login package

This commit is contained in:
Maxim Lebedev 2020-09-04 21:42:56 +05:00
parent 2ac03cb7b9
commit d76ec735d4
No known key found for this signature in database
GPG Key ID: F8978F46FF0FFA4F
6 changed files with 206 additions and 150 deletions

4
go.mod
View File

@ -4,14 +4,14 @@ go 1.12
require (
github.com/Masterminds/semver v1.5.0
github.com/fasthttp/router v1.3.2
github.com/json-iterator/go v1.1.9
github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9
github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc // indirect
github.com/klauspost/compress v1.10.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/valyala/fasthttp v1.14.0
github.com/valyala/fasthttp v1.16.0
golang.org/x/text v0.3.2
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
)

12
go.sum
View File

@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/router v1.3.2 h1:n9r5QNuJi5z5Sp2vp/0SrawogTjGfYFqTOyP/R8ehNI=
github.com/fasthttp/router v1.3.2/go.mod h1:athTSKMdel0Qhh3W4nB8qn+EPYuyj6YZMUo6ZcXWTgc=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -13,7 +15,6 @@ github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9 h1:mA7k8E2Vrmyj
github.com/kirillDanshin/dlog v0.0.0-20170728000807-97d876b12bf9/go.mod h1:l8CN7iyX1k2xlsTYVTpCtwBPcxThf/jLWDGVcF6T/bM=
github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc h1:OkOhOn3WBUmfATC1NsA3rBlgHGkjk0KGnR5akl/8uXc=
github.com/kirillDanshin/myutils v0.0.0-20160713214838-182269b1fbcc/go.mod h1:Bt95qRxLvpdmASW9s2tTxGdQ5ma4o4n8QFhCvzCew/M=
github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -24,18 +25,21 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c h1:KKqhycXW1WVNkX7r4ekTV2gFkbhdyihlWD8c0/FiWmk=
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.14.0 h1:67bfuW9azCMwW/Jlq/C+VeihNpAuJMWkYPBig1gdi3A=
github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE=
github.com/valyala/fasthttp v1.16.0 h1:9zAqOYLl8Tuy3E5R6ckzGDJ1g8+pw15oQp2iL9Jl6gQ=
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View File

@ -1,44 +1,68 @@
package login_test
/*
import (
"fmt"
"log"
httprouter "github.com/buaazp/fasthttprouter"
"github.com/fasthttp/router"
http "github.com/valyala/fasthttp"
"gitlab.com/toby3d/telegram/login"
)
const htmlTemplate string = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Telegram login</title>
</head>
<body>
<script async src="https://telegram.org/js/telegram-widget.js?11" data-telegram-login="toby3dBot"
data-size="large" data-auth-url="https://example.site/callback" data-request-access="write"></script>
</body>
</html>`
func Example_fastStart() {
// We use bot AccessToken from @BotFather or telegram.Bot structure
botAccessToken := "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
// Use bot AccessToken from @BotFather as ClientSecret.
c := login.Config{
ClientSecret: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
RedirectURL: "https://example.site/callback",
RequestWriteAccess: true,
}
// Create example server with example callback handler
r := httprouter.New()
// Create example server with authorization and token (callback) handlers.
r := router.New()
r.GET("/", func(ctx *http.RequestCtx) {
// Render page with embeded Telegram Login button (until Telegram enable the possibility of login by
// link.)
ctx.SuccessString("text/html", htmlTemplate)
// NOTE(toby3d): Telegram does not yet allow you to login without script via a link, as is common
// in traditional OAuth2 applications, stopping at the last step with redirect to callback. The
// 'embed=[0|1]' parameter has no effect now, which is very similar to a bug.
//ctx.SuccessString("text/html", fmt.Sprintf(htmlTemplate, c.AuthCodeURL(language.English)))
})
r.GET("/callback", func(ctx *http.RequestCtx) {
defer ctx.SetConnectionClose()
q := ctx.QueryArgs()
u := login.User{
AuthDate: int64(q.GetUintOrZero(login.KeyAuthDate)),
FirstName: string(q.Peek(login.KeyFirstName)),
Hash: string(q.Peek(login.KeyHash)),
ID: q.GetUintOrZero(login.KeyID),
LastName: string(q.Peek(login.KeyLastName)),
PhotoURL: string(q.Peek(login.KeyPhotoURL)),
Username: string(q.Peek(login.KeyUsername)),
}
// You not need decode data to User structure if you want only
// validate it
u, err := login.ParseUser(ctx.QueryArgs())
if err != nil {
ctx.Error("bad request", http.StatusBadRequest)
if !c.Verify(&u) {
ctx.Error("Unable to verify data", http.StatusUnauthorized)
return
}
// Check User structure
ok, err := login.CheckAuthorization(u, botAccessToken)
if err != nil || !ok { // NANI!? It's a invalid data!
ctx.Error("bad request", http.StatusBadRequest)
return
}
// All is ok! Hello, human!
ctx.Success("text/html", []byte("hello, "+u.FullName()+"!"))
ctx.SuccessString("text/plain", fmt.Sprintf("Hello, %s!", u.FullName()))
})
if err := http.ListenAndServe(":8000", r.Handler); err != nil {
if err := http.ListenAndServe(":80", r.Handler); err != nil {
log.Fatalln(err.Error())
}
}
*/

View File

@ -4,56 +4,98 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"strings"
http "github.com/valyala/fasthttp"
"golang.org/x/text/language"
)
type (
Widget struct {
accessToken string
Config struct {
// ClientSecret is the bot token.
ClientSecret string
// RedirectURL is the URL to redirect users going through the login flow.
RedirectURL string
// RequestWriteAccess request the permission for bot to send messages to the user.
RequestWriteAccess bool
}
// 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"`
ID int `json:"id"`
LastName string `json:"last_name,omitempty"`
PhotoURL string `json:"photo_url,omitempty"`
Username string `json:"username,omitempty"`
}
)
const Endpoint string = "https://oauth.telegram.org/auth"
// 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"
KeyAuthDate string = "auth_date"
KeyFirstName string = "first_name"
KeyHash string = "hash"
KeyID string = "id"
KeyLastName string = "last_name"
KeyPhotoURL string = "photo_url"
KeyUsername string = "username"
)
func NewWidget(accessToken string) *Widget {
return &Widget{accessToken: accessToken}
// ClientID returns bot ID from it's ClientSecret token.
func (c Config) ClientID() string {
return strings.SplitN(c.ClientSecret, ":", 2)[0]
}
// 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
// AuthCodeURL returns a URL to Telegram login page that asks for permissions for the required scopes explicitly.
func (c *Config) AuthCodeURL(lang language.Tag) string {
origin, _ := url.Parse(c.RedirectURL)
u := http.AcquireURI()
defer http.ReleaseURI(u)
u.Update(Endpoint)
a := u.QueryArgs()
a.Set("bot_id", c.ClientID())
a.Set("origin", origin.Scheme+"://"+origin.Host)
a.Add("embed", "0")
if lang != language.Und {
a.Set("lang", lang.String())
}
if c.RequestWriteAccess {
a.Set("request_access", "write")
}
return u.String()
}
func (w Widget) GenerateHash(u User) (string, error) {
// Verify 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 (c *Config) Verify(u *User) bool {
if u == nil || u.Hash == "" {
return false
}
h, err := generateHash(c.ClientSecret, u)
return err == nil && u.Hash == h
}
func generateHash(token string, u *User) (string, error) {
a := http.AcquireArgs()
defer http.ReleaseArgs(a)
// WARN: do not change order of this args, it must be alphabetical
// WARN(toby3d): 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)
@ -70,7 +112,7 @@ func (w Widget) GenerateHash(u User) (string, error) {
a.Set(KeyUsername, u.Username)
}
secretKey := sha256.Sum256([]byte(w.accessToken))
secretKey := sha256.Sum256([]byte(token))
h := hmac.New(sha256.New, secretKey[0:])
if _, err := h.Write(a.QueryString()); err != nil {

View File

@ -1,54 +1,38 @@
package login
package login_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gitlab.com/toby3d/telegram/login"
"golang.org/x/text/language"
)
func TestNew(t *testing.T) {
assert.NotNil(t, NewWidget("hackme"))
func TestClientID(t *testing.T) {
c := login.Config{ClientSecret: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"}
assert.Equal(t, "123456", c.ClientID())
}
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(),
func TestAuthCodeURL(t *testing.T) {
c := login.Config{
ClientSecret: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
RedirectURL: "https://example.site/callback",
RequestWriteAccess: true,
}
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)
})
assert.Equal(t, "https://oauth.telegram.org/auth?bot_id=123456&origin=https%3A%2F%2Fexample.site"+
"&embed=0&lang=ru&request_access=write", c.AuthCodeURL(language.Russian))
}
func TestVerify(t *testing.T) {
c := login.Config{ClientSecret: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"}
assert.True(t, c.Verify(&login.User{
ID: 123456,
Username: "toby3d",
FirstName: "Maxim",
LastName: "Lebedev",
PhotoURL: "https://t.me/i/userpic/320/ABC-DEF1234ghIkl-zyx57W2v1u123ew11.jpg",
AuthDate: 1410696795,
Hash: "d9b74e929cd4cfa7299031421db61949ecd49641c3b06e3a0361f593cf1fe064",
}))
}

View File

@ -7,64 +7,66 @@ import (
"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())
})
func TestUserFullName(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.Empty(t, u.FullName())
})
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("first name", func(t *testing.T) {
u := User{
FirstName: "Maxim",
}
assert.Equal(t, u.FirstName, u.FullName())
})
t.Run("auth time", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.True(t, u.AuthTime().IsZero())
})
t.Run("exists", func(t *testing.T) {
u := User{AuthDate: time.Now().UTC().Unix()}
assert.False(t, u.AuthTime().IsZero())
})
})
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())
})
t.Run("first & last name", func(t *testing.T) {
u := User{
FirstName: "Maxim",
LastName: "Lebedev",
}
assert.Equal(t, u.FirstName+" "+u.LastName, u.FullName())
})
}
func TestUserAuthTime(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var u User
assert.True(t, u.AuthTime().IsZero())
})
t.Run("exists", func(t *testing.T) {
u := User{AuthDate: time.Now().UTC().Unix()}
assert.False(t, u.AuthTime().IsZero())
})
}
func TestUserhasLastName(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())
})
}
func TestUserHasUsername(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())
})
}
func TestUserHasPhoto(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())
})
}