Create a PKCE auth strategy based on goth

This commit is contained in:
Blake Imsland 2020-10-27 16:04:47 -07:00
Родитель 4a94e3d917
Коммит 1874ac5180
7 изменённых файлов: 418 добавлений и 4 удалений

4
go.mod
Просмотреть файл

@ -5,9 +5,13 @@ go 1.14
require (
cloud.google.com/go/storage v1.12.0
github.com/gobuffalo/flect v0.2.2
github.com/jarcoal/httpmock v1.0.6
github.com/justinas/alice v1.2.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/markbates/goth v1.65.0
github.com/rs/zerolog v1.20.0
github.com/stretchr/testify v1.6.1
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
google.golang.org/api v0.34.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
)

29
go.sum
Просмотреть файл

@ -13,8 +13,9 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.66.0 h1:DZeAkuQGQqnm9Xv36SbMJEU8aFBz4wL04UpMWPWwjzg=
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko=
cloud.google.com/go v0.67.0 h1:YIkzmqUfVGiGPpT98L8sVvUIkDno6UlrDxw4NR6z5ak=
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -45,6 +46,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -104,9 +107,16 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@ -118,7 +128,12 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.65.0/go.mod h1:65frybxoeSCfORin51KOKqAKbIh7wREIDvdCkdWj//4=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -128,6 +143,7 @@ github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -199,8 +215,10 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 h1:YfxMZzv3PjGonQYNUaeU2+DhAdqOxerQ30JFB6WgAXo=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -297,8 +315,9 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266 h1:k7tVuG0g1JwmD3Jh8oAl1vQ1C3jb4Hi/dUl1wWDBJpQ=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f h1:18s2P7JILnVhIF2+ZtGJQ9czV5bvTsb13/UGtNPDbjA=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -362,8 +381,9 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5 h1:B9nroC8SSX5GtbVvxPF9tYIVkaCpjhVLOrlAY8ONzm8=
google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe h1:6SgESkjJknFUnsfQ2yxQbmTAi37BxhwS/riq+VdLo9c=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -394,6 +414,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

161
pkce/pkce.go Normal file
Просмотреть файл

@ -0,0 +1,161 @@
package pkce
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
const (
authPath = "/authorize"
tokenPath = "/oauth/token"
profilePath = "/userinfo"
protocol = "https://"
)
type Provider struct {
*oauth2.Config
ProfileURL string
name string
}
type UserInfo struct {
Email string `json:"email"`
Name string `json:"name"`
NickName string `json:"nickname"`
UserID string `json:"sub"`
}
func New(clientID, redirectURI, domain string, scopes ...string) *Provider {
p := &Provider{
Config: &oauth2.Config{
ClientID: clientID,
RedirectURL: redirectURI,
Endpoint: oauth2.Endpoint{
AuthURL: protocol + domain + authPath,
TokenURL: protocol + domain + tokenPath,
},
},
ProfileURL: protocol + domain + profilePath,
name: "pkce",
}
if len(scopes) > 0 {
p.Config.Scopes = make([]string, len(scopes))
for i, scope := range scopes {
p.Config.Scopes[i] = scope
}
} else {
p.Config.Scopes = []string{"openid", "profile", "email"}
}
return p
}
func (p *Provider) Name() string {
return p.name
}
func (p *Provider) SetName(name string) {
p.name = name
}
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
cv, err := codeVerifier()
if err != nil {
return nil, err
}
cc := codeChallenge(cv)
s := &Session{
AuthURL: p.Config.AuthCodeURL(
state,
oauth2.SetAuthURLParam("code_challenge", cc),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
),
CodeVerifier: cv,
}
return s, nil
}
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
s := session.(*Session)
user := goth.User{
AccessToken: s.AccessToken,
Provider: p.Name(),
RefreshToken: s.RefreshToken,
ExpiresAt: s.ExpiresAt,
}
if user.AccessToken == "" {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.Name())
}
req, err := http.NewRequest(http.MethodGet, p.ProfileURL, nil)
if err != nil {
return user, err
}
req.Header.Set("Authorization", "Bearer "+s.AccessToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
if resp.Body != nil {
resp.Body.Close()
}
return user, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d while trying to fetch user information", p.Name(), resp.StatusCode)
}
var rawData map[string]interface{}
userInfo := &UserInfo{}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return user, err
}
err = json.Unmarshal(body, &rawData)
if err != nil {
return user, err
}
err = json.Unmarshal(body, userInfo)
if err != nil {
return user, err
}
user.Email = userInfo.Email
user.Name = userInfo.Name
user.NickName = userInfo.NickName
user.RawData = rawData
user.UserID = userInfo.UserID
return user, nil
}
func (p *Provider) Debug(_ bool) {}
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
token := &oauth2.Token{
RefreshToken: refreshToken,
}
tokenSource := p.Config.TokenSource(context.Background(), token)
newToken, err := tokenSource.Token()
if err != nil {
return nil, err
}
return newToken, nil
}
func (p *Provider) RefreshTokenAvailable() bool {
return true
}

108
pkce/pkce_test.go Normal file
Просмотреть файл

@ -0,0 +1,108 @@
package pkce_test
import (
"fmt"
"testing"
"github.com/jarcoal/httpmock"
"github.com/markbates/goth"
"github.com/mozilla/protodash/pkce"
"github.com/stretchr/testify/assert"
)
const (
pkceDomain = "pkce.example.com"
pkceClientID = "some-client-id"
pkceRedirectURI = "/callback"
)
func TestNew(t *testing.T) {
p := provider()
expectedProfileURL := fmt.Sprintf("https://%s/userinfo", pkceDomain)
assert.Equal(t, pkceClientID, p.ClientID)
assert.Equal(t, expectedProfileURL, p.ProfileURL)
assert.Equal(t, pkceRedirectURI, p.RedirectURL)
}
func TestImplementsProvider(t *testing.T) {
p := provider()
assert.Implements(t, (*goth.Provider)(nil), p)
}
func TestBeginAuth(t *testing.T) {
p := provider()
expectedAuthURL := fmt.Sprintf("https://%s/authorize", pkceDomain)
session, err := p.BeginAuth("test_state")
assert.NoError(t, err)
s := session.(*pkce.Session)
assert.Contains(t, s.AuthURL, expectedAuthURL)
}
func TestUnmarshalSession(t *testing.T) {
p := provider()
expectedAuthURL := "https://" + pkceDomain + "/oauth/authorize"
sessionResp := fmt.Sprintf(`{"AuthURL":"%s","AccessToken":"1234567890"}`, expectedAuthURL)
session, err := p.UnmarshalSession(sessionResp)
assert.NoError(t, err)
s := session.(*pkce.Session)
assert.Equal(t, expectedAuthURL, s.AuthURL)
assert.Equal(t, "1234567890", s.AccessToken)
}
func TestFetchUser(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
sampleResp := `{
"email_verified": false,
"email": "test.account@userinfo.com",
"clientID": "q2hnj2iu...",
"updated_at": "2016-12-05T15:15:40.545Z",
"name": "test.account@userinfo.com",
"picture": "https://s.gravatar.com/avatar/dummy.png",
"user_id": "auth0|58454...",
"nickname": "test.account",
"identities": [
{
"user_id": "58454...",
"provider": "auth0",
"connection": "Username-Password-Authentication",
"isSocial": false
}],
"created_at": "2016-12-05T11:16:59.640Z",
"sub": "auth0|58454..."
}`
httpmock.RegisterResponder(
"GET",
fmt.Sprintf("https://%s/userinfo", pkceDomain),
httpmock.NewStringResponder(200, sampleResp),
)
p := provider()
session, _ := p.BeginAuth("test_state")
s := session.(*pkce.Session)
s.AccessToken = "token"
u, err := p.FetchUser(s)
assert.NoError(t, err)
assert.Equal(t, "test.account@userinfo.com", u.Email)
assert.Equal(t, "auth0|58454...", u.UserID)
assert.Equal(t, "test.account", u.NickName)
assert.Equal(t, "test.account@userinfo.com", u.Name)
assert.Equal(t, "token", u.AccessToken)
}
func provider() *pkce.Provider {
return pkce.New(
pkceClientID,
pkceRedirectURI,
pkceDomain,
)
}

66
pkce/session.go Normal file
Просмотреть файл

@ -0,0 +1,66 @@
package pkce
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
type Session struct {
AuthURL string
AccessToken string
RefreshToken string
ExpiresAt time.Time
CodeVerifier string
}
// GetAuthURL returns the URL for the authentication end-point for the provider.
func (s *Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Marshal generates a string representation of the Session for storing between requests.
func (s *Session) Marshal() string {
buf, _ := json.Marshal(s)
return string(buf)
}
// Authorize should validate the data from the provider and return an access token
// that can be stored for later access to the provider.
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
p := provider.(*Provider)
token, err := p.Config.Exchange(
context.Background(),
params.Get("code"),
oauth2.SetAuthURLParam("code_verifier", s.CodeVerifier),
)
if err != nil {
return "", err
}
if !token.Valid() {
return "", errors.New("invalid token received from provider")
}
s.AccessToken = token.AccessToken
s.RefreshToken = token.RefreshToken
s.ExpiresAt = token.Expiry
return token.AccessToken, nil
}
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
s := &Session{}
if err := json.Unmarshal([]byte(data), s); err != nil {
return nil, err
}
return s, nil
}

31
pkce/session_test.go Normal file
Просмотреть файл

@ -0,0 +1,31 @@
package pkce_test
import (
"testing"
"github.com/markbates/goth"
"github.com/mozilla/protodash/pkce"
"github.com/stretchr/testify/assert"
)
func TestImplementsSession(t *testing.T) {
s := &pkce.Session{}
assert.Implements(t, (*goth.Session)(nil), s)
}
func TestGetAuthURL(t *testing.T) {
s := &pkce.Session{}
_, err := s.GetAuthURL()
assert.Error(t, err)
s.AuthURL = "/foo"
url, _ := s.GetAuthURL()
assert.Equal(t, "/foo", url)
}
func TestMarshal(t *testing.T) {
s := &pkce.Session{}
data := s.Marshal()
assert.Equal(t, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z","CodeVerifier":""}`, data)
}

23
pkce/utils.go Normal file
Просмотреть файл

@ -0,0 +1,23 @@
package pkce
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
func codeVerifier() (string, error) {
bs := make([]byte, 32)
_, err := rand.Read(bs)
if err != nil {
return "", err
}
str := base64.RawURLEncoding.EncodeToString(bs)
return str, nil
}
func codeChallenge(verifier string) string {
bs := sha256.Sum256([]byte(verifier))
str := base64.RawURLEncoding.EncodeToString(bs[:])
return str
}