vednor auth v0.2.0

This commit is contained in:
Umputun
2019-01-02 00:16:37 -06:00
parent ca1943f909
commit 17acbc6322
9 changed files with 155 additions and 15 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}