revendor with auth:0.1.1
This commit is contained in:
Generated
+3
-3
@@ -112,8 +112,7 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:495f89256b4fef47e64160d109454685afa4949f400d02833207e2af383d1173"
|
||||
digest = "1:2e6b942dd80c33bba11b9567f3f8a4338e80d597959e4a5d182d5251953bebe6"
|
||||
name = "github.com/go-pkgz/auth"
|
||||
packages = [
|
||||
".",
|
||||
@@ -124,7 +123,8 @@
|
||||
"token",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "46d3a954882f41fc0efdf551e2c1b070799e0f54"
|
||||
revision = "a4dab49e2656a32ab7eb1255980c24f8560dc298"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574"
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestServerApp_DevMode(t *testing.T) {
|
||||
time.Sleep(100 * time.Millisecond) // let server start
|
||||
|
||||
assert.Equal(t, 4+1, len(app.restSrv.Authenticator.Providers()), "extra auth provider")
|
||||
assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name, "dev auth provider")
|
||||
assert.Equal(t, "dev", app.restSrv.Authenticator.Providers()[4].Name(), "dev auth provider")
|
||||
// send ping
|
||||
resp, err := http.Get("http://localhost:18085/api/v1/ping")
|
||||
require.Nil(t, err)
|
||||
|
||||
@@ -238,7 +238,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cnf.Auth = []string{}
|
||||
for _, ap := range s.Authenticator.Providers() {
|
||||
cnf.Auth = append(cnf.Auth, ap.Name)
|
||||
cnf.Auth = append(cnf.Auth, ap.Name())
|
||||
}
|
||||
|
||||
if cnf.Admins == nil { // prevent json serialization to nil
|
||||
|
||||
+14
-14
@@ -13,9 +13,10 @@ This library provides "social login" with Github, Google, Facebook and Yandex.
|
||||
- Black list with user-defined validator
|
||||
- Multiple aud (audience) supported
|
||||
- Secure key with customizable `SecretReader`
|
||||
- Ability to store extra information to token and retrieve on login
|
||||
- Ability to store an extra information to token and retrieve on login
|
||||
- Pre-auth and post-auth hooks to handle custom use cases.
|
||||
- Middleware for easy integration into http routers
|
||||
- Wrappers to extract user info from the request
|
||||
|
||||
## Install
|
||||
|
||||
@@ -107,35 +108,35 @@ It also has placeholders for fields application can populate with custom `token.
|
||||
|
||||
- `IP` - hash of user's IP address
|
||||
- `Email` - user's email
|
||||
- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details.
|
||||
- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provided, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details.
|
||||
|
||||
|
||||
### Avatar proxy
|
||||
|
||||
Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy
|
||||
Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides an automatic proxy
|
||||
|
||||
- On each login the proxy will retrieve user's picture and save it to `AvatarStore`
|
||||
- Local (proxied) link to avatar included in user's info (jwt token)
|
||||
- API for avatar removal provided as a part of `AvatarStore`
|
||||
- User can leverage one of provided stores:
|
||||
- User can leverage one of the provided stores:
|
||||
- `avatar.LocalFS` - file system, each avatar in a separate file
|
||||
- `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store).
|
||||
- `avatar.BoltDB` - single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store).
|
||||
- `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db).
|
||||
- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25).
|
||||
- In case of need custom implementations of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25).
|
||||
- All avatar-related setup done as a part of `auth.Opts` and needs:
|
||||
- `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")`
|
||||
- `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info")
|
||||
- `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing.
|
||||
- `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links like this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info")
|
||||
- `AvatarResizeLimit` - size (in pixels) used to resize the avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing.
|
||||
|
||||
### Customization
|
||||
|
||||
There are several ways to adjust functionality of the library:
|
||||
|
||||
1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification
|
||||
1. `SecretReader` - interface with a single method `Get(aud string) string` to return the secret used for JWT signing and verification
|
||||
1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on.
|
||||
2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users.
|
||||
|
||||
All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`.
|
||||
All of the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`.
|
||||
|
||||
### Implementing black list logic or some other filters
|
||||
|
||||
@@ -157,11 +158,10 @@ Working with oauth2 providers can be a pain, especially during development phase
|
||||
```go
|
||||
// runs dev oauth2 server on :8084
|
||||
go func() {
|
||||
p, err := service.Provider("dev")
|
||||
devAuthServer, err := service.DevAuth()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
devAuthServer := provider.DevAuthServer{Provider: p}
|
||||
devAuthServer.Run()
|
||||
}()
|
||||
```
|
||||
@@ -175,7 +175,7 @@ _Warning: this is not the real oauth2 server but just a small fake thing for dev
|
||||
In addition to the primary method (i.e. JWT cookie with XSRF header) there are two more ways to authenticate:
|
||||
|
||||
1. Send JWT header as `X-JWT`. This shouldn't be used for web application, however can be helpful for service-to-service authentication.
|
||||
2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode by default disabled and will be enabled it `Opts.AdminPasswd` defined. This will allow access with basic auth admin:<Opts.AdminPasswd> with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts.
|
||||
2. [Basic access authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). This mode disabled by default and will be enabled if `Opts.AdminPasswd` defined. This will allow access with basic auth admin:<Opts.AdminPasswd> with user [admin](https://github.com/go-pkgz/auth/blob/master/middleware/auth.go#L24). Such method can be used for automation scripts.
|
||||
|
||||
### Logging
|
||||
|
||||
@@ -239,4 +239,4 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co
|
||||
|
||||
The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine.
|
||||
|
||||
`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible.
|
||||
`go-pkgz/auth` library still in development and until version 1 released some breaking changes possible.
|
||||
+8
-8
@@ -131,7 +131,7 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl
|
||||
if elems[len(elems)-1] == "list" {
|
||||
list := []string{}
|
||||
for _, p := range s.providers {
|
||||
list = append(list, p.Name)
|
||||
list = append(list, p.Name())
|
||||
}
|
||||
rest.RenderJSON(w, r, list)
|
||||
return
|
||||
@@ -189,15 +189,15 @@ func (s *Service) AddProvider(name string, cid string, csecret string) {
|
||||
|
||||
switch strings.ToLower(name) {
|
||||
case "github":
|
||||
s.providers = append(s.providers, provider.NewGithub(p))
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewGithub(p)))
|
||||
case "google":
|
||||
s.providers = append(s.providers, provider.NewGoogle(p))
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewGoogle(p)))
|
||||
case "facebook":
|
||||
s.providers = append(s.providers, provider.NewFacebook(p))
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p)))
|
||||
case "yandex":
|
||||
s.providers = append(s.providers, provider.NewFacebook(p))
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewFacebook(p)))
|
||||
case "dev":
|
||||
s.providers = append(s.providers, provider.NewDev(p))
|
||||
s.providers = append(s.providers, provider.NewService(provider.NewDev(p)))
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -212,13 +212,13 @@ func (s *Service) DevAuth() (*provider.DevAuthServer, error) {
|
||||
return nil, errors.Wrap(err, "dev provider not registered")
|
||||
}
|
||||
// make and start dev auth server
|
||||
return &provider.DevAuthServer{Provider: p, L: s.logger}, nil
|
||||
return &provider.DevAuthServer{Provider: p.Provider.(provider.Oauth2Handler), L: s.logger}, nil
|
||||
}
|
||||
|
||||
// Provider gets provider by name
|
||||
func (s *Service) Provider(name string) (provider.Service, error) {
|
||||
for _, p := range s.providers {
|
||||
if p.Name == name {
|
||||
if p.Name() == name {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -27,7 +27,7 @@ const devAuthPort = 8084
|
||||
// desired user name, this is the mode used for development. Non-interactive mode for tests only.
|
||||
type DevAuthServer struct {
|
||||
logger.L
|
||||
Provider Service
|
||||
Provider Oauth2Handler
|
||||
Automatic bool
|
||||
username string // unsafe, but fine for dev
|
||||
|
||||
@@ -39,7 +39,7 @@ type DevAuthServer struct {
|
||||
// Run oauth2 dev server on port devAuthPort
|
||||
func (d *DevAuthServer) Run(ctx context.Context) {
|
||||
d.username = "dev_user"
|
||||
d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.RedirectURL)
|
||||
d.Logf("[INFO] run local oauth2 dev server on %d, redir url=%s", devAuthPort, d.Provider.redirectURL)
|
||||
d.lock.Lock()
|
||||
var err error
|
||||
d.iconGen, err = identicon.New("github", 5, 3)
|
||||
@@ -77,7 +77,7 @@ func (d *DevAuthServer) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
state := r.URL.Query().Get("state")
|
||||
callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.RedirectURL, state)
|
||||
callbackURL := fmt.Sprintf("%s?code=g0ZGZmNjVmOWI&state=%s", d.Provider.redirectURL, state)
|
||||
d.Logf("[DEBUG] callback url=%s", callbackURL)
|
||||
w.Header().Add("Location", callbackURL)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
@@ -150,17 +150,17 @@ func (d *DevAuthServer) Shutdown() {
|
||||
}
|
||||
|
||||
// NewDev makes dev oauth2 provider for admin user
|
||||
func NewDev(p Params) Service {
|
||||
return initService(p, Service{
|
||||
Name: "dev",
|
||||
Endpoint: oauth2.Endpoint{
|
||||
func NewDev(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "dev",
|
||||
endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/authorize", devAuthPort),
|
||||
TokenURL: fmt.Sprintf("http://127.0.0.1:%d/login/oauth/access_token", devAuthPort),
|
||||
},
|
||||
RedirectURL: p.URL + "/auth/dev/callback",
|
||||
Scopes: []string{"user:email"},
|
||||
InfoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort),
|
||||
MapUser: func(data userData, _ []byte) token.User {
|
||||
redirectURL: p.URL + "/auth/dev/callback",
|
||||
scopes: []string{"user:email"},
|
||||
infoURL: fmt.Sprintf("http://127.0.0.1:%d/user", devAuthPort),
|
||||
mapUser: func(data userData, _ []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: data.value("id"),
|
||||
Name: data.value("name"),
|
||||
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/rest"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
)
|
||||
|
||||
// Oauth2Handler implements /login, /callback and /logout handlers from aouth2 flow
|
||||
type Oauth2Handler struct {
|
||||
Params
|
||||
|
||||
// all of these fields specific to particular oauth2 provider
|
||||
name string
|
||||
redirectURL string
|
||||
infoURL string
|
||||
endpoint oauth2.Endpoint
|
||||
scopes []string
|
||||
mapUser func(userData, []byte) token.User // map info from InfoURL to User
|
||||
conf oauth2.Config
|
||||
}
|
||||
|
||||
// Params to make initialized and ready to use provider
|
||||
type Params struct {
|
||||
logger.L
|
||||
URL string
|
||||
JwtService TokenService
|
||||
Cid string
|
||||
Csecret string
|
||||
Issuer string
|
||||
AvatarSaver AvatarSaver
|
||||
}
|
||||
|
||||
type userData map[string]interface{}
|
||||
|
||||
func (u userData) value(key string) string {
|
||||
// json.Unmarshal converts json "null" value to go's "nil", in this case return empty string
|
||||
if val, ok := u[key]; ok && val != nil {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// initOauth2Handler makes oauth2 handler for given provider
|
||||
func initOauth2Handler(p Params, service Oauth2Handler) Oauth2Handler {
|
||||
if p.L == nil {
|
||||
p.L = logger.Func(func(fmt string, args ...interface{}) {})
|
||||
}
|
||||
p.Logf("[INFO] init oauth2 service %s", service.name)
|
||||
service.Params = p
|
||||
service.conf = oauth2.Config{
|
||||
ClientID: service.Cid,
|
||||
ClientSecret: service.Csecret,
|
||||
RedirectURL: service.redirectURL,
|
||||
Scopes: service.scopes,
|
||||
Endpoint: service.endpoint,
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s",
|
||||
service.name, service.Cid, service.endpoint, service.redirectURL)
|
||||
return service
|
||||
}
|
||||
|
||||
// Name returns provider name
|
||||
func (p Oauth2Handler) Name() string { return p.name }
|
||||
|
||||
// LoginHandler - GET /login?from=redirect-back-url&site=siteID&session=1
|
||||
func (p Oauth2Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
p.Logf("[DEBUG] login with %s", p.Name)
|
||||
// make state (random) and store in session
|
||||
state, err := randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state")
|
||||
return
|
||||
}
|
||||
|
||||
cid, err := randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id")
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims{
|
||||
Handshake: &token.Handshake{
|
||||
State: state,
|
||||
From: r.URL.Query().Get("from"),
|
||||
},
|
||||
SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0",
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: cid,
|
||||
Audience: r.URL.Query().Get("site"),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := p.JwtService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
|
||||
// return login url
|
||||
loginURL := p.conf.AuthCodeURL(state)
|
||||
p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims)
|
||||
|
||||
http.Redirect(w, r, loginURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// AuthHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
|
||||
// GET /callback
|
||||
func (p Oauth2Handler) AuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
oauthClaims, _, err := p.JwtService.Get(r)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get token")
|
||||
return
|
||||
}
|
||||
|
||||
if oauthClaims.Handshake == nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "invalid handshake token")
|
||||
return
|
||||
}
|
||||
|
||||
retrievedState := oauthClaims.Handshake.State
|
||||
if retrievedState == "" || retrievedState != r.URL.Query().Get("state") {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state")
|
||||
return
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] token with state %s", retrievedState)
|
||||
tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed")
|
||||
return
|
||||
}
|
||||
|
||||
client := p.conf.Client(context.Background(), tok)
|
||||
uinfo, err := client.Get(p.infoURL)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info")
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := uinfo.Body.Close(); e != nil {
|
||||
p.Logf("[WARN] failed to close response body, %s", e)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := ioutil.ReadAll(uinfo.Body)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info")
|
||||
return
|
||||
}
|
||||
|
||||
jData := map[string]interface{}{}
|
||||
if e := json.Unmarshal(data, &jData); e != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info")
|
||||
return
|
||||
}
|
||||
p.Logf("[DEBUG] got raw user info %+v", jData)
|
||||
|
||||
u := p.mapUser(jData, data)
|
||||
u, err = setAvatar(p.AvatarSaver, u)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save avatar to proxy")
|
||||
return
|
||||
}
|
||||
|
||||
cid, err := randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id")
|
||||
return
|
||||
}
|
||||
claims := token.Claims{
|
||||
User: &u,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: p.Issuer,
|
||||
Id: cid,
|
||||
Audience: oauthClaims.Audience,
|
||||
},
|
||||
SessionOnly: oauthClaims.SessionOnly,
|
||||
}
|
||||
|
||||
if err = p.JwtService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] user info %+v", u)
|
||||
|
||||
// redirect to back url if presented in login query params
|
||||
if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" {
|
||||
http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
rest.RenderJSON(w, r, &u)
|
||||
}
|
||||
|
||||
// LogoutHandler - GET /logout
|
||||
func (p Oauth2Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
p.JwtService.Reset(w)
|
||||
}
|
||||
+32
-32
@@ -14,14 +14,14 @@ import (
|
||||
)
|
||||
|
||||
// NewGoogle makes google oauth2 provider
|
||||
func NewGoogle(p Params) Service {
|
||||
return initService(p, Service{
|
||||
Name: "google",
|
||||
Endpoint: google.Endpoint,
|
||||
RedirectURL: p.URL + "/auth/google/callback",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"},
|
||||
InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
MapUser: func(data userData, _ []byte) token.User {
|
||||
func NewGoogle(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "google",
|
||||
endpoint: google.Endpoint,
|
||||
redirectURL: p.URL + "/auth/google/callback",
|
||||
scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"},
|
||||
infoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
mapUser: func(data userData, _ []byte) token.User {
|
||||
userInfo := token.User{
|
||||
// encode email with provider name to avoid collision if same id returned by other provider
|
||||
ID: "google_" + token.HashID(sha1.New(), data.value("sub")),
|
||||
@@ -37,14 +37,14 @@ func NewGoogle(p Params) Service {
|
||||
}
|
||||
|
||||
// NewGithub makes github oauth2 provider
|
||||
func NewGithub(p Params) Service {
|
||||
return initService(p, Service{
|
||||
Name: "github",
|
||||
Endpoint: github.Endpoint,
|
||||
RedirectURL: p.URL + "/auth/github/callback",
|
||||
Scopes: []string{},
|
||||
InfoURL: "https://api.github.com/user",
|
||||
MapUser: func(data userData, _ []byte) token.User {
|
||||
func NewGithub(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "github",
|
||||
endpoint: github.Endpoint,
|
||||
redirectURL: p.URL + "/auth/github/callback",
|
||||
scopes: []string{},
|
||||
infoURL: "https://api.github.com/user",
|
||||
mapUser: func(data userData, _ []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: "github_" + token.HashID(sha1.New(), data.value("login")),
|
||||
Name: data.value("name"),
|
||||
@@ -60,7 +60,7 @@ func NewGithub(p Params) Service {
|
||||
}
|
||||
|
||||
// NewFacebook makes facebook oauth2 provider
|
||||
func NewFacebook(p Params) Service {
|
||||
func NewFacebook(p Params) Oauth2Handler {
|
||||
|
||||
// response format for fb /me call
|
||||
type uinfo struct {
|
||||
@@ -73,13 +73,13 @@ func NewFacebook(p Params) Service {
|
||||
} `json:"picture"`
|
||||
}
|
||||
|
||||
return initService(p, Service{
|
||||
Name: "facebook",
|
||||
Endpoint: facebook.Endpoint,
|
||||
RedirectURL: p.URL + "/auth/facebook/callback",
|
||||
Scopes: []string{"public_profile"},
|
||||
InfoURL: "https://graph.facebook.com/me?fields=id,name,picture",
|
||||
MapUser: func(data userData, bdata []byte) token.User {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "facebook",
|
||||
endpoint: facebook.Endpoint,
|
||||
redirectURL: p.URL + "/auth/facebook/callback",
|
||||
scopes: []string{"public_profile"},
|
||||
infoURL: "https://graph.facebook.com/me?fields=id,name,picture",
|
||||
mapUser: func(data userData, bdata []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: "facebook_" + token.HashID(sha1.New(), data.value("id")),
|
||||
Name: data.value("name"),
|
||||
@@ -98,15 +98,15 @@ func NewFacebook(p Params) Service {
|
||||
}
|
||||
|
||||
// NewYandex makes yandex oauth2 provider
|
||||
func NewYandex(p Params) Service {
|
||||
return initService(p, Service{
|
||||
Name: "yandex",
|
||||
Endpoint: yandex.Endpoint,
|
||||
RedirectURL: p.URL + "/auth/yandex/callback",
|
||||
Scopes: []string{},
|
||||
func NewYandex(p Params) Oauth2Handler {
|
||||
return initOauth2Handler(p, Oauth2Handler{
|
||||
name: "yandex",
|
||||
endpoint: yandex.Endpoint,
|
||||
redirectURL: p.URL + "/auth/yandex/callback",
|
||||
scopes: []string{},
|
||||
// See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/
|
||||
InfoURL: "https://login.yandex.ru/info?format=json",
|
||||
MapUser: func(data userData, _ []byte) token.User {
|
||||
infoURL: "https://login.yandex.ru/info?format=json",
|
||||
mapUser: func(data userData, _ []byte) token.User {
|
||||
userInfo := token.User{
|
||||
ID: "yandex_" + token.HashID(sha1.New(), data.value("id")),
|
||||
Name: data.value("display_name"), // using Display Name by default
|
||||
|
||||
+22
-199
@@ -1,47 +1,25 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/rest"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/go-pkgz/auth/token"
|
||||
)
|
||||
|
||||
// Service represents oauth2 provider
|
||||
// Service represents oauth2 provider. Adds Handler method multiplexing login, auth and logout requests
|
||||
type Service struct {
|
||||
Params
|
||||
Name string
|
||||
RedirectURL string
|
||||
InfoURL string
|
||||
Endpoint oauth2.Endpoint
|
||||
Scopes []string
|
||||
MapUser func(userData, []byte) token.User // map info from InfoURL to User
|
||||
conf oauth2.Config
|
||||
Provider
|
||||
}
|
||||
|
||||
// Params to make initialized and ready to use provider
|
||||
type Params struct {
|
||||
logger.L
|
||||
URL string
|
||||
JwtService TokenService
|
||||
AvatarSaver AvatarSaver
|
||||
Cid string
|
||||
Csecret string
|
||||
Issuer string
|
||||
// NewService makes service for given provider
|
||||
func NewService(p Provider) Service {
|
||||
return Service{Provider: p}
|
||||
}
|
||||
|
||||
// AvatarSaver defines minimal interface to save avatar
|
||||
@@ -57,34 +35,12 @@ type TokenService interface {
|
||||
Reset(w http.ResponseWriter)
|
||||
}
|
||||
|
||||
type userData map[string]interface{}
|
||||
|
||||
func (u userData) value(key string) string {
|
||||
// json.Unmarshal converts json "null" value to go's "nil", in this case return empty string
|
||||
if val, ok := u[key]; ok && val != nil {
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// initService makes oauth2 service for given provider
|
||||
func initService(p Params, service Service) Service {
|
||||
if p.L == nil {
|
||||
p.L = logger.Func(func(fmt string, args ...interface{}) {})
|
||||
}
|
||||
p.Logf("[INFO] init oauth2 service %s", service.Name)
|
||||
service.Params = p
|
||||
service.conf = oauth2.Config{
|
||||
ClientID: service.Cid,
|
||||
ClientSecret: service.Csecret,
|
||||
RedirectURL: service.RedirectURL,
|
||||
Scopes: service.Scopes,
|
||||
Endpoint: service.Endpoint,
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s",
|
||||
service.Name, service.Cid, service.Endpoint, service.RedirectURL)
|
||||
return service
|
||||
// Provider defines interface for auth handler
|
||||
type Provider interface {
|
||||
Name() string
|
||||
LoginHandler(w http.ResponseWriter, r *http.Request)
|
||||
AuthHandler(w http.ResponseWriter, r *http.Request)
|
||||
LogoutHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Handler returns auth routes for given provider
|
||||
@@ -95,11 +51,11 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/login") {
|
||||
p.loginHandler(w, r)
|
||||
p.LoginHandler(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/callback") {
|
||||
p.authHandler(w, r)
|
||||
p.AuthHandler(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(r.URL.Path, "/logout") {
|
||||
@@ -109,153 +65,20 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// loginHandler - GET /login?from=redirect-back-url&site=siteID&session=1
|
||||
func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
p.Logf("[DEBUG] login with %s", p.Name)
|
||||
// make state (random) and store in session
|
||||
state, err := p.randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make oauth2 state")
|
||||
return
|
||||
}
|
||||
|
||||
cid, err := p.randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id")
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims{
|
||||
Handshake: &token.Handshake{
|
||||
State: state,
|
||||
From: r.URL.Query().Get("from"),
|
||||
},
|
||||
SessionOnly: r.URL.Query().Get("session") != "" && r.URL.Query().Get("session") != "0",
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Id: cid,
|
||||
Audience: r.URL.Query().Get("site"),
|
||||
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
|
||||
NotBefore: time.Now().Add(-1 * time.Minute).Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := p.JwtService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
|
||||
// return login url
|
||||
loginURL := p.conf.AuthCodeURL(state)
|
||||
p.Logf("[DEBUG] login url %s, claims=%+v", loginURL, claims)
|
||||
|
||||
http.Redirect(w, r, loginURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// authHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser
|
||||
// GET /callback
|
||||
func (p Service) authHandler(w http.ResponseWriter, r *http.Request) {
|
||||
oauthClaims, _, err := p.JwtService.Get(r)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to get token")
|
||||
return
|
||||
}
|
||||
|
||||
if oauthClaims.Handshake == nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token")
|
||||
return
|
||||
}
|
||||
|
||||
retrievedState := oauthClaims.Handshake.State
|
||||
if retrievedState == "" || retrievedState != r.URL.Query().Get("state") {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state")
|
||||
return
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] token with state %s", retrievedState)
|
||||
tok, err := p.conf.Exchange(context.Background(), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "exchange failed")
|
||||
return
|
||||
}
|
||||
|
||||
client := p.conf.Client(context.Background(), tok)
|
||||
uinfo, err := client.Get(p.InfoURL)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info")
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if e := uinfo.Body.Close(); e != nil {
|
||||
p.Logf("[WARN] failed to close response body, %s", e)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := ioutil.ReadAll(uinfo.Body)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to read user info")
|
||||
return
|
||||
}
|
||||
|
||||
jData := map[string]interface{}{}
|
||||
if e := json.Unmarshal(data, &jData); e != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to unmarshal user info")
|
||||
return
|
||||
}
|
||||
p.Logf("[DEBUG] got raw user info %+v", jData)
|
||||
|
||||
u := p.MapUser(jData, data)
|
||||
u = p.setAvatar(u)
|
||||
|
||||
cid, err := p.randToken()
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to make claim's id")
|
||||
return
|
||||
}
|
||||
claims := token.Claims{
|
||||
User: &u,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: p.Issuer,
|
||||
Id: cid,
|
||||
Audience: oauthClaims.Audience,
|
||||
},
|
||||
SessionOnly: oauthClaims.SessionOnly,
|
||||
}
|
||||
|
||||
if err = p.JwtService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
|
||||
p.Logf("[DEBUG] user info %+v", u)
|
||||
|
||||
// redirect to back url if presented in login query params
|
||||
if oauthClaims.Handshake != nil && oauthClaims.Handshake.From != "" {
|
||||
http.Redirect(w, r, oauthClaims.Handshake.From, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
rest.RenderJSON(w, r, &u)
|
||||
}
|
||||
|
||||
// setAvatar saves avatar and puts proxied URL to u.Picture
|
||||
func (p Service) setAvatar(u token.User) token.User {
|
||||
if p.AvatarSaver != nil {
|
||||
if avatarURL, e := p.AvatarSaver.Put(u); e == nil {
|
||||
u.Picture = avatarURL
|
||||
} else {
|
||||
p.Logf("[WARN] failed to set avatar for %+v, %+v", u, e)
|
||||
func setAvatar(ava AvatarSaver, u token.User) (token.User, error) {
|
||||
if ava != nil {
|
||||
avatarURL, e := ava.Put(u)
|
||||
if e != nil {
|
||||
return u, errors.Wrap(e, "failed to save avatar for")
|
||||
}
|
||||
u.Picture = avatarURL
|
||||
return u, nil
|
||||
}
|
||||
return u
|
||||
return u, nil // empty AvatarSaver ok, just skipped
|
||||
}
|
||||
|
||||
// LogoutHandler - GET /logout
|
||||
func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
p.JwtService.Reset(w)
|
||||
}
|
||||
|
||||
func (p Service) randToken() (string, error) {
|
||||
func randToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", errors.Wrap(err, "can't get random")
|
||||
|
||||
Reference in New Issue
Block a user