-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce opaque token strategy
- Loading branch information
Showing
6 changed files
with
664 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
// Copyright 2020 The Go-Guardian. All rights reserved. | ||
// Use of this source code is governed by a MIT | ||
// license that can be found in the LICENSE file. | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
|
||
"log" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
"github.com/gorilla/mux" | ||
|
||
"github.com/shaj13/libcache" | ||
_ "github.com/shaj13/libcache/fifo" | ||
_ "github.com/shaj13/libcache/idle" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/basic" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/opaque" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/token" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/union" | ||
) | ||
|
||
// Usage: | ||
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -u admin:admin | ||
// curl -k http://127.0.0.1:8080/v1/auth/token -u admin:admin <obtain a token> | ||
// curl -k http://127.0.0.1:8080/v1/auth/token -H "Authorization: Bearer <refresh token>" | ||
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -H "Authorization: Bearer <token>" | ||
|
||
func main() { | ||
a := newAuthenticator() | ||
router := mux.NewRouter() | ||
router.HandleFunc("/v1/auth/token", a.middleware(http.HandlerFunc(a.createToken))).Methods("GET") | ||
router.HandleFunc("/v1/book/{id}", a.middleware(http.HandlerFunc(getBookAuthor))).Methods("GET") | ||
log.Println("server started and listening on http://127.0.0.1:8080") | ||
http.ListenAndServe("127.0.0.1:8080", router) | ||
} | ||
|
||
func getBookAuthor(w http.ResponseWriter, r *http.Request) { | ||
vars := mux.Vars(r) | ||
id := vars["id"] | ||
books := map[string]string{ | ||
"1449311601": "Ryan Boyd", | ||
"148425094X": "Yvonne Wilson", | ||
"1484220498": "Prabath Siriwarden", | ||
} | ||
body := fmt.Sprintf("Author: %s \n", books[id]) | ||
w.Write([]byte(body)) | ||
} | ||
|
||
func newAuthenticator() *authenticator { | ||
cache := libcache.FIFO.New(0) | ||
db := &db{m: make(map[string]opaque.Token)} | ||
secret := opaque.StaticSecret([]byte("secret")) | ||
|
||
refreshScope := token.NewScope("refresh", "/v1/auth/token", "GET") | ||
refreshOpts := []auth.Option{ | ||
opaque.WithExpDuration(time.Hour * 30 * 24), | ||
opaque.WithTokenPrefix("r"), | ||
token.SetScopes(refreshScope), | ||
} | ||
|
||
basicStrategy := basic.NewCached(validateUser, cache) | ||
accessStrategy := opaque.New(cache, db, secret) | ||
// Use IDLE to prevent caching one time refresh token. | ||
refreshStrategy := opaque.New(libcache.IDLE.New(0), db, secret, refreshOpts...) | ||
union := union.New(accessStrategy, refreshStrategy, basicStrategy) | ||
|
||
return &authenticator{ | ||
union: union, | ||
refresh: refreshOpts, | ||
secret: secret, | ||
store: db, | ||
} | ||
} | ||
|
||
func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) { | ||
// here connect to db or any other service to fetch user and validate it. | ||
if userName == "admin" && password == "admin" { | ||
return auth.NewDefaultUser("admin", "1", nil, nil), nil | ||
} | ||
|
||
return nil, fmt.Errorf("Invalid credentials") | ||
} | ||
|
||
type authenticator struct { | ||
union union.Union | ||
secret opaque.SecretsKeeper | ||
store opaque.TokenStore | ||
access []auth.Option | ||
refresh []auth.Option | ||
} | ||
|
||
func (a *authenticator) createToken(w http.ResponseWriter, r *http.Request) { | ||
user := auth.User(r) | ||
token.WithNamedScopes(user, "refresh") // limit refresh token usage See newAuthenticator. | ||
|
||
accessToken, err := opaque.IssueToken(r.Context(), user, a.store, a.secret, a.access...) | ||
if err != nil { | ||
http.Error(w, err.Error(), 500) | ||
return | ||
} | ||
|
||
refreshToken, err := opaque.IssueToken(r.Context(), user, a.store, a.secret, a.refresh...) | ||
if err != nil { | ||
http.Error(w, err.Error(), 500) | ||
return | ||
} | ||
|
||
resp := struct { | ||
AccessToken string | ||
RefreshToken string | ||
}{ | ||
AccessToken: accessToken, | ||
RefreshToken: refreshToken, | ||
} | ||
|
||
buf, _ := json.Marshal(resp) | ||
w.Write(buf) | ||
} | ||
|
||
func (a *authenticator) middleware(next http.Handler) http.HandlerFunc { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
log.Println("Executing Auth Middleware") | ||
_, user, err := a.union.AuthenticateRequest(r) | ||
if err != nil { | ||
log.Println(err) | ||
code := http.StatusUnauthorized | ||
http.Error(w, http.StatusText(code), code) | ||
return | ||
} | ||
log.Printf("User %s Authenticated\n", user.GetUserName()) | ||
r = auth.RequestWithUser(user, r) | ||
next.ServeHTTP(w, r) | ||
}) | ||
} | ||
|
||
type db struct { | ||
mu sync.Mutex | ||
m map[string]opaque.Token | ||
} | ||
|
||
func (db db) Store(_ context.Context, t opaque.Token) error { | ||
db.mu.Lock() | ||
defer db.mu.Unlock() | ||
db.m[t.Signature] = t | ||
return nil | ||
} | ||
|
||
func (db db) Lookup(_ context.Context, sig string) (opaque.Token, error) { | ||
db.mu.Lock() | ||
defer db.mu.Unlock() | ||
|
||
t, ok := db.m[sig] | ||
|
||
if !ok { | ||
return opaque.Token{}, errors.New("db: token not found") | ||
} | ||
|
||
if t.Prefix == "r" { | ||
// Refresh token is one time password so remove it | ||
// To prevent user use it again. | ||
delete(db.m, sig) | ||
} | ||
|
||
return t, nil | ||
} | ||
|
||
func (db db) Revoke(ctx context.Context, sig string) error { | ||
return errors.New("revoke not implemented") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// Package opaque provides server-side consistent tokens. | ||
// | ||
// It generates tokens in a proprietary format that the | ||
// client cannot access and contain some identifier to | ||
// information in a server's persistent storage. | ||
// | ||
// It uses HMAC with SHA to generate and validate tokens. | ||
package opaque | ||
|
||
import ( | ||
"context" | ||
"crypto" | ||
"crypto/hmac" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/shaj13/go-guardian/v2/auth" | ||
"github.com/shaj13/go-guardian/v2/auth/strategies/token" | ||
) | ||
|
||
// SecretsKeeper hold all secrets/keys to sign and parse opaque token. | ||
type SecretsKeeper interface { | ||
// Keys return's keys to sign and parse opaque token, | ||
// The Returned keys must be in descending order timestamp. | ||
Keys() ([][]byte, error) | ||
} | ||
|
||
// StaticSecret implements the SecretsKeeper and holds only a single secret. | ||
type StaticSecret []byte | ||
|
||
// Keys return's keys to sign and parse opaque token, | ||
func (s StaticSecret) Keys() ([][]byte, error) { | ||
return [][]byte{s}, nil | ||
} | ||
|
||
// TokenStore is used to manage client tokens. Tokens are used for | ||
// clients to authenticate, and each token is mapped to an applicable auth info. | ||
type TokenStore interface { | ||
// Store used to store a new token entry. | ||
Store(context.Context, Token) error | ||
// Lookup used to get token entry by its signature. | ||
Lookup(ctx context.Context, signature string) (Token, error) | ||
// Revoke used to delete token entry by its signature. | ||
Revoke(ctx context.Context, signature string) error | ||
} | ||
|
||
// Token represent a token entry in token store. | ||
type Token struct { | ||
// Lifespan represent when the token expires. | ||
Lifespan time.Time | ||
// Signature a unique HMAC, per token. | ||
// | ||
// Signature used to verify client token. | ||
// | ||
// Store the signature in plaintext without | ||
// any form of obfuscation or encryption. | ||
Signature string | ||
// Prefix represent token prefix or type. | ||
Prefix string | ||
// Info represent auth info token is mapped to it. | ||
Info auth.Info | ||
} | ||
|
||
// IssueToken issue token for the provided user info. | ||
func IssueToken( | ||
ctx context.Context, | ||
info auth.Info, | ||
s TokenStore, | ||
k SecretsKeeper, | ||
opts ...auth.Option, | ||
) (string, error) { | ||
return newOpaque(s, k, opts...).issue(ctx, info) | ||
} | ||
|
||
// GetAuthenticateFunc return function to authenticate request using opaque token. | ||
// The returned function typically used with the token strategy. | ||
func GetAuthenticateFunc(s TokenStore, k SecretsKeeper, opts ...auth.Option) token.AuthenticateFunc { | ||
return newOpaque(s, k, opts...).parse | ||
} | ||
|
||
// New return strategy authenticate request using opaque token. | ||
// | ||
// New is similar to: | ||
// | ||
// fn := opaque.GetAuthenticateFunc(tokenStore, secretsKeeper, opts...) | ||
// token.New(fn, cache, opts...) | ||
func New(c auth.Cache, s TokenStore, k SecretsKeeper, opts ...auth.Option) auth.Strategy { | ||
fn := GetAuthenticateFunc(s, k, opts...) | ||
return token.New(fn, c, opts...) | ||
} | ||
|
||
func newOpaque(s TokenStore, k SecretsKeeper, opts ...auth.Option) *opaque { | ||
o := &opaque{ | ||
tokenLength: 24, | ||
prefix: "s", | ||
exp: time.Hour * 24, | ||
keeper: k, | ||
store: s, | ||
h: crypto.SHA512_256, | ||
} | ||
|
||
for _, opt := range opts { | ||
opt.Apply(o) | ||
} | ||
|
||
return o | ||
} | ||
|
||
type opaque struct { | ||
tokenLength int | ||
prefix string | ||
exp time.Duration | ||
keeper SecretsKeeper | ||
store TokenStore | ||
h crypto.Hash | ||
} | ||
|
||
func (o *opaque) issue(ctx context.Context, info auth.Info) (string, error) { | ||
id := make([]byte, o.tokenLength) | ||
if _, err := io.ReadFull(rand.Reader, id); err != nil { | ||
return "", err | ||
} | ||
|
||
keys, err := o.keeper.Keys() | ||
|
||
if len(keys) == 0 || err != nil { | ||
return "", fmt.Errorf("strategies/opaque: no key to sign token %w", err) | ||
} | ||
|
||
signature := o.sign(keys[0], id) | ||
|
||
t := Token{ | ||
Prefix: o.prefix, | ||
Lifespan: time.Now().Add(o.exp), | ||
Info: info, | ||
Signature: base64.RawURLEncoding.EncodeToString(signature), | ||
} | ||
|
||
if err := o.store.Store(ctx, t); err != nil { | ||
return "", err | ||
} | ||
|
||
mixed := append(id, signature...) | ||
return o.prefix + "." + base64.RawURLEncoding.EncodeToString(mixed), nil | ||
} | ||
|
||
func (o *opaque) parse(ctx context.Context, _ *http.Request, token string) (auth.Info, time.Time, error) { | ||
if len(token) <= (len(o.prefix) + o.tokenLength + 1) { | ||
return nil, time.Time{}, errors.New("strategies/opaque: token is too short") | ||
} | ||
|
||
if token[:len(o.prefix)] != o.prefix { | ||
return nil, time.Time{}, errors.New("strategies/opaque: invalid token prefix") | ||
} | ||
|
||
mixed, err := base64.RawURLEncoding.DecodeString(token[len(o.prefix)+1:]) | ||
if err != nil { | ||
return nil, time.Time{}, err | ||
} | ||
|
||
keys, err := o.keeper.Keys() | ||
if len(keys) == 0 || err != nil { | ||
return nil, time.Time{}, fmt.Errorf("strategies/opaque: no key to sign token %w", err) | ||
} | ||
|
||
id := mixed[:o.tokenLength] | ||
signature := mixed[o.tokenLength:] | ||
ok := false | ||
|
||
for _, key := range keys { | ||
mac := o.sign(key, id) | ||
if ok = hmac.Equal(mac, signature); ok { | ||
break | ||
} | ||
} | ||
|
||
if !ok { | ||
return nil, time.Time{}, errors.New("strategies/opaque: invalid token signature") | ||
} | ||
|
||
t, err := o.store.Lookup(ctx, base64.RawURLEncoding.EncodeToString(signature)) | ||
if err != nil { | ||
return nil, time.Time{}, err | ||
} | ||
|
||
if t.Lifespan.Before(time.Now()) { | ||
return nil, time.Time{}, errors.New("strategies/opaque: token is expired") | ||
} | ||
|
||
return t.Info, t.Lifespan, nil | ||
} | ||
|
||
func (o *opaque) sign(key, id []byte) []byte { | ||
hm := hmac.New(o.h.New, key) | ||
_, _ = hm.Write(id) | ||
return hm.Sum(nil) | ||
} |
Oops, something went wrong.