diff --git a/go.mod b/go.mod
index 0c6c8a8..b2d026b 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 3a539ee..fe9d54d 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/login/example_test.go b/login/example_test.go
index 15ad462..5ff17c4 100644
--- a/login/example_test.go
+++ b/login/example_test.go
@@ -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 = `
+
+
+
+
+ Telegram login
+
+
+
+
+`
+
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())
}
}
-*/
diff --git a/login/login.go b/login/login.go
index b5abc4d..1d0c280 100644
--- a/login/login.go
+++ b/login/login.go
@@ -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 {
diff --git a/login/login_test.go b/login/login_test.go
index b71ba9b..51b364b 100644
--- a/login/login_test.go
+++ b/login/login_test.go
@@ -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",
+ }))
}
diff --git a/login/utils_test.go b/login/utils_test.go
index a384272..4571ec4 100644
--- a/login/utils_test.go
+++ b/login/utils_test.go
@@ -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())
})
}