vednor auth v0.2.0
This commit is contained in:
Generated
+3
-3
@@ -112,7 +112,7 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2e6b942dd80c33bba11b9567f3f8a4338e80d597959e4a5d182d5251953bebe6"
|
||||
digest = "1:a4ff2b649472abf046975396ac916b04527fde8d897857c2feea76498aeb762f"
|
||||
name = "github.com/go-pkgz/auth"
|
||||
packages = [
|
||||
".",
|
||||
@@ -123,8 +123,8 @@
|
||||
"token",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a4dab49e2656a32ab7eb1255980c24f8560dc298"
|
||||
version = "v0.1.1"
|
||||
revision = "855a238343c3bcea84b352fdeb4393576f9eb217"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574"
|
||||
|
||||
+19
-3
@@ -2,12 +2,13 @@
|
||||
|
||||
|
||||
|
||||
This library provides "social login" with Github, Google, Facebook and Yandex.
|
||||
This library provides "social login" with Github, Google, Facebook and Yandex as well as custom auth providers.
|
||||
|
||||
- Multiple oauth2 providers can be used at the same time
|
||||
- Special `dev` provider allows local testing and development
|
||||
- JWT stored in a secure cookie with XSRF protection. Cookies can be session-only
|
||||
- Minimal scopes with user name, id and picture (avatar) only
|
||||
- Direct authentication with user's provided credential checker
|
||||
- Integrated avatar proxy with FS, boltdb and gridfs storages
|
||||
- Support of user-defined storages for avatars
|
||||
- Black list with user-defined validator
|
||||
@@ -128,13 +129,28 @@ Direct links to avatars won't survive any real-life usage if they linked from a
|
||||
- `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.
|
||||
|
||||
### Direct authentication
|
||||
|
||||
In addition to oauth2 providers `auth.Service` allows to use direct user-defined authentication. This is done by adding direct provider with `auth.AddDirectProvider`.
|
||||
|
||||
```go
|
||||
service.AddDirectProvider("local", provider.CredCheckerFunc(func(user, password string) (ok bool, err error) {
|
||||
ok, err := checkUserSomehow(user, password)
|
||||
return ok, err
|
||||
}))
|
||||
```
|
||||
|
||||
Such provider acts like any other, i.e. will be registered as `/auth/local/login`.
|
||||
|
||||
The API for this provider - `GET /auth/<name>/login?user=<user>&passwd=<password>&aud=<site_id>&session=[1|0]`
|
||||
|
||||
### Customization
|
||||
|
||||
There are several ways to adjust functionality of the library:
|
||||
|
||||
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.
|
||||
2. `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.
|
||||
3. `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 the interfaces above have corresponding Func adapters - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`.
|
||||
|
||||
|
||||
+18
-1
@@ -34,7 +34,9 @@ type Opts struct {
|
||||
SecureCookies bool // makes jwt cookie secure
|
||||
TokenDuration time.Duration // token's TTL, refreshed automatically
|
||||
CookieDuration time.Duration // cookie's TTL. This cookie stores JWT token
|
||||
DisableXSRF bool // disable XSRF protection, useful for testing/debugging
|
||||
|
||||
DisableXSRF bool // disable XSRF protection, useful for testing/debugging
|
||||
DisableIAT bool // disable IssuedAt claim
|
||||
|
||||
// optional (custom) names for cookies and headers
|
||||
JWTCookieName string // default "JWT"
|
||||
@@ -83,6 +85,7 @@ func NewService(opts Opts) (res *Service) {
|
||||
TokenDuration: opts.TokenDuration,
|
||||
CookieDuration: opts.CookieDuration,
|
||||
DisableXSRF: opts.DisableXSRF,
|
||||
DisableIAT: opts.DisableIAT,
|
||||
JWTCookieName: opts.JWTCookieName,
|
||||
JWTHeaderKey: opts.JWTHeaderKey,
|
||||
XSRFCookieName: opts.XSRFCookieName,
|
||||
@@ -205,6 +208,20 @@ func (s *Service) AddProvider(name string, cid string, csecret string) {
|
||||
s.authMiddleware.Providers = s.providers
|
||||
}
|
||||
|
||||
// AddDirectProvider adds provider with direct check against data store
|
||||
// it doesn't do any handshake and uses provided credChecker to verify user and password from the request
|
||||
func (s *Service) AddDirectProvider(name string, credChecker provider.CredChecker) {
|
||||
dh := provider.DirectHandler{
|
||||
L: s.logger,
|
||||
ProviderName: name,
|
||||
Issuer: s.issuer,
|
||||
TokenService: s.jwtService,
|
||||
CredChecker: credChecker,
|
||||
}
|
||||
s.providers = append(s.providers, provider.NewService(dh))
|
||||
s.authMiddleware.Providers = s.providers
|
||||
}
|
||||
|
||||
// DevAuth makes dev oauth2 server, for testing and development only!
|
||||
func (s *Service) DevAuth() (*provider.DevAuthServer, error) {
|
||||
p, err := s.Provider("dev") // peak dev provider
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ github.com/go-pkgz/rest v1.1.4 h1:/Lrg9kBWBjNah7nmCDHLszRAfVVBIy5ajf0vVgpHPi0=
|
||||
github.com/go-pkgz/rest v1.1.4/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
|
||||
github.com/go-pkgz/rest v1.1.5 h1:5br4mnscfLb27yxv5hJFLBVmAt09PrmIBP+meA3CfHc=
|
||||
github.com/go-pkgz/rest v1.1.5/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
|
||||
|
||||
+2
-2
@@ -126,9 +126,9 @@ func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token.
|
||||
}
|
||||
|
||||
// AdminOnly middleware allows access for admins only
|
||||
// this handler internally wrapped with auth(true) to avoid situation if AdminOnly defined without prior Auth
|
||||
func (a *Authenticator) AdminOnly(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user, err := token.GetUserInfo(r)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
@@ -141,7 +141,7 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler {
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
return a.auth(true)(http.HandlerFunc(fn)) // enforce auth
|
||||
}
|
||||
|
||||
// basic auth for admin user
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-pkgz/rest"
|
||||
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
)
|
||||
|
||||
// DirectHandler implements non-oauth2 provider authorizing user in traditional way with storage
|
||||
// with users and hashes
|
||||
type DirectHandler struct {
|
||||
logger.L
|
||||
CredChecker CredChecker
|
||||
ProviderName string
|
||||
TokenService TokenService
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// CredChecker defines interface to check credentials
|
||||
type CredChecker interface {
|
||||
Check(user, password string) (ok bool, err error)
|
||||
}
|
||||
|
||||
// CredCheckerFunc type is an adapter to allow the use of ordinary functions as CredsChecker.
|
||||
type CredCheckerFunc func(user, password string) (ok bool, err error)
|
||||
|
||||
// Check calls f(user,passwd)
|
||||
func (f CredCheckerFunc) Check(user, password string) (ok bool, err error) {
|
||||
return f(user, password)
|
||||
}
|
||||
|
||||
// Name of the handler
|
||||
func (p DirectHandler) Name() string { return p.ProviderName }
|
||||
|
||||
// LoginHandler checks "user" and "passwd" against data store and makes jwt if all passed
|
||||
// GET /something?user=name&password=xyz&sess=[0|1]
|
||||
func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
user, password := r.URL.Query().Get("user"), r.URL.Query().Get("passwd")
|
||||
aud := r.URL.Query().Get("aud")
|
||||
sessOnly := r.URL.Query().Get("sess") == "1"
|
||||
if p.CredChecker == nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errors.New("empty credential store"), "no credential store")
|
||||
return
|
||||
}
|
||||
ok, err := p.CredChecker.Check(user, password)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to access creds store")
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "incorrect user or password")
|
||||
return
|
||||
}
|
||||
claims := token.Claims{
|
||||
User: &token.User{Name: user},
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: p.Issuer,
|
||||
Audience: aud,
|
||||
},
|
||||
SessionOnly: sessOnly,
|
||||
}
|
||||
|
||||
if err = p.TokenService.Set(w, claims); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token")
|
||||
return
|
||||
}
|
||||
rest.RenderJSON(w, r, claims.User)
|
||||
}
|
||||
|
||||
// AuthHandler doesn't do anyting for direct login as it has no callbacks
|
||||
func (p DirectHandler) AuthHandler(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
// LogoutHandler - GET /logout
|
||||
func (p DirectHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
p.TokenService.Reset(w)
|
||||
}
|
||||
+1
-1
@@ -77,7 +77,7 @@ 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)
|
||||
p.Logf("[DEBUG] login with %s", p.Name())
|
||||
// make state (random) and store in session
|
||||
state, err := randToken()
|
||||
if err != nil {
|
||||
|
||||
+26
-2
@@ -1,6 +1,8 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -48,7 +50,7 @@ type Opts struct {
|
||||
TokenDuration time.Duration
|
||||
CookieDuration time.Duration
|
||||
DisableXSRF bool
|
||||
|
||||
DisableIAT bool // disable IssuedAt claim
|
||||
// optional (custom) names for cookies and headers
|
||||
JWTCookieName string
|
||||
JWTHeaderKey string
|
||||
@@ -95,6 +97,10 @@ func (j *Service) Token(claims Claims) (string, error) {
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
if j.SecretReader == nil {
|
||||
return "", errors.New("secretreader not defined")
|
||||
}
|
||||
|
||||
secret, err := j.SecretReader.Get(claims.Audience) // get secret via consumer defined SecretReader
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "can't get secret")
|
||||
@@ -128,6 +134,10 @@ func (j *Service) Parse(tokenString string) (Claims, error) {
|
||||
return Claims{}, errors.Wrap(err, "failed to get aud from token token")
|
||||
}
|
||||
|
||||
if j.SecretReader == nil {
|
||||
return Claims{}, errors.New("secretreader not defined")
|
||||
}
|
||||
|
||||
secret, err := j.SecretReader.Get(aud)
|
||||
if err != nil {
|
||||
return Claims{}, errors.Wrap(err, "can't get secret")
|
||||
@@ -159,7 +169,13 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims) error {
|
||||
claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix()
|
||||
}
|
||||
|
||||
claims.Issuer = j.Issuer
|
||||
if claims.Issuer == "" {
|
||||
claims.Issuer = j.Issuer
|
||||
}
|
||||
|
||||
if !j.DisableIAT {
|
||||
claims.IssuedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
tokenString, err := j.Token(claims)
|
||||
if err != nil {
|
||||
@@ -280,3 +296,11 @@ type ValidatorFunc func(token string, claims Claims) bool
|
||||
func (f ValidatorFunc) Validate(token string, claims Claims) bool {
|
||||
return f(token, claims)
|
||||
}
|
||||
|
||||
func (c Claims) String() string {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%+v %+v", c.StandardClaims, c.User)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
+4
-3
@@ -7,7 +7,6 @@ import (
|
||||
"hash"
|
||||
"hash/crc64"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
@@ -74,7 +73,7 @@ func (u *User) IsAdmin() bool {
|
||||
return u.BoolAttr(adminAttr)
|
||||
}
|
||||
|
||||
// HashID tries to has val with hash.Hash and fallback to crc if needed
|
||||
// HashID tries to hash val with hash.Hash and fallback to crc if needed
|
||||
func HashID(h hash.Hash, val string) string {
|
||||
|
||||
if reValidSha.MatchString(val) {
|
||||
@@ -83,7 +82,9 @@ func HashID(h hash.Hash, val string) string {
|
||||
|
||||
if _, err := io.WriteString(h, val); err != nil {
|
||||
// fail back to crc64
|
||||
log.Printf("[WARN] can't hash id %s, %s", val, err)
|
||||
if val == "" {
|
||||
val = "!empty string!"
|
||||
}
|
||||
if reValidCrc64.MatchString(val) {
|
||||
return val // already crced
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user