authorize on a per-endpoint basis. Change name of CLIs

This commit is contained in:
Dan Laine 2020-06-29 12:54:43 -04:00
parent 0d7b224e5f
commit 2640977cde
5 changed files with 101 additions and 29 deletions

View File

@ -13,8 +13,6 @@ import (
jwt "github.com/dgrijalva/jwt-go"
)
// TODO: Add method to revoke a token
const (
headerKey = "Authorization"
headerValStart = "Bearer "
@ -41,6 +39,15 @@ type Auth struct {
revoked []string // List of tokens that have been revoked
}
// Custom claim type used for API access token
type endpointClaims struct {
// Each element is an endpoint that the token allows access to
// If endpoints has an element "*", allows access to all API endpoints
// In this case, "*" should be the only element of [endpoints]
Endpoints []string
jwt.StandardClaims
}
// getToken gets the JWT token from the request header
// Assumes the header is this form:
// "Authorization": "Bearer TOKEN.GOES.HERE"
@ -55,17 +62,34 @@ func getToken(r *http.Request) (string, error) {
return rawHeader[len(headerValStart):], nil // Returns actual auth token. Slice guaranteed to not go OOB
}
// Create and return a new token
func (auth *Auth) newToken(password string) (string, error) {
// Create and return a new token that allows access to each API endpoint such
// that the API's path ends with an element of [endpoints]
// If one of the elements of [endpoints] is "*", allows access to all APIs
func (auth *Auth) newToken(password string, endpoints []string) (string, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
if password != auth.Password {
return "", errWrongPassword
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ // Make a new token
ExpiresAt: time.Now().Add(TokenLifespan).Unix(),
})
return token.SignedString([]byte(auth.Password)) // Sign it
canAccessAll := false
for _, endpoint := range endpoints {
if endpoint == "*" {
canAccessAll = true
break
}
}
claims := endpointClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(TokenLifespan).Unix(),
},
}
if canAccessAll {
claims.Endpoints = []string{"*"}
} else {
claims.Endpoints = endpoints
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(auth.Password)) // Sign the token and return its string repr.
}
@ -101,7 +125,7 @@ func (auth *Auth) changePassword(oldPassword, newPassword string) error {
if auth.Password != oldPassword {
return errWrongPassword
} else if len(newPassword) == 0 || len(newPassword) > maxPasswordLen {
return fmt.Errorf("password length exceeds maximum length, %d", maxPasswordLen)
return fmt.Errorf("new password length exceeds maximum length, %d", maxPasswordLen)
} else if oldPassword == newPassword {
return errors.New("new password can't be same as old password")
}
@ -133,12 +157,12 @@ func (auth *Auth) WrapHandler(h http.Handler) http.Handler {
return
}
token, err := jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) { // See if token is well-formed and signature is right
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) { // See if token is well-formed and signature is right
auth.lock.RLock()
defer auth.lock.RUnlock()
return []byte(auth.Password), nil
})
if err != nil { // Signature is probably wrong
if err != nil { // Probably because signature wrong
w.WriteHeader(http.StatusUnauthorized)
io.WriteString(w, fmt.Sprintf("invalid auth token: %s", err))
return
@ -148,14 +172,43 @@ func (auth *Auth) WrapHandler(h http.Handler) http.Handler {
io.WriteString(w, "invalid auth token. Is it expired?")
return
}
// Make sure this token gives access to the requested endpoint
claims, ok := token.Claims.(*endpointClaims)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
io.WriteString(w, "expected auth token's claims to be type endpointClaims but is different type")
return
}
if l := len(claims.Endpoints); l < 1 || l > maxEndpoints {
w.WriteHeader(http.StatusUnauthorized)
io.WriteString(w, fmt.Sprintf("expected auth token to allow access to between %d and %d endpoints, but does %d", 1, maxEndpoints, l))
return
}
canAccess := false // true iff the token authorizes access to the API
for _, endpoint := range claims.Endpoints {
if endpoint == "*" || strings.HasSuffix(r.URL.Path, endpoint) {
canAccess = true
break
}
}
if !canAccess {
w.WriteHeader(http.StatusUnauthorized)
io.WriteString(w, "the provided auth token does not allow access to this endpoint")
return
}
auth.lock.RLock()
for _, revokedToken := range auth.revoked { // Make sure this token wasn't revoked
if revokedToken == tokenStr {
w.WriteHeader(http.StatusUnauthorized)
io.WriteString(w, "this token was revoked")
io.WriteString(w, "the provided auth token was revoked")
auth.lock.RUnlock()
return
}
}
auth.lock.RUnlock()
h.ServeHTTP(w, r) // Authentication successful
h.ServeHTTP(w, r) // Authorization successful
})
}

View File

@ -11,6 +11,10 @@ import (
"github.com/ava-labs/gecko/utils/logging"
)
const (
maxEndpoints = 128
)
// Service ...
type Service struct {
log logging.Logger
@ -37,18 +41,33 @@ type Password struct {
Password string `json:"password"` // The authotization password
}
// NewTokenArgs ...
type NewTokenArgs struct {
Password
// Endpoints that may be accessed with this token
// e.g. if endpoints is ["/ext/bc/X", "/ext/admin"] then the token holder
// can hit the X-Chain API and the admin API
// If [Endpoints] contains an element "*" then the token
// allows access to all API endpoints
// [Endpoints] must have between 1 and [maxEndpoints] elements
Endpoints []string `json:"endpoints"`
}
// Token ...
type Token struct {
Token string `json:"token"` // The new token. Expires in [TokenLifespan].
}
// NewToken returns a new token
func (s *Service) NewToken(_ *http.Request, args *Password, reply *Token) error {
func (s *Service) NewToken(_ *http.Request, args *NewTokenArgs, reply *Token) error {
s.log.Info("Auth: NewToken called")
if args.Password == "" {
return fmt.Errorf("password not given")
if args.Password.Password == "" {
return fmt.Errorf("argument 'password' not given")
}
token, err := s.newToken(args.Password)
if l := len(args.Endpoints); l < 1 || l > maxEndpoints {
return fmt.Errorf("argument 'endpoints' must have between %d and %d elements, but has %d", 1, maxEndpoints, l)
}
token, err := s.newToken(args.Password.Password, args.Endpoints)
reply.Token = token
return err
}

View File

@ -197,8 +197,8 @@ func init() {
fs.BoolVar(&Config.EnableHTTPS, "http-tls-enabled", false, "Upgrade the HTTP server to HTTPs")
fs.StringVar(&Config.HTTPSKeyFile, "http-tls-key-file", "", "TLS private key file for the HTTPs server")
fs.StringVar(&Config.HTTPSCertFile, "http-tls-cert-file", "", "TLS certificate file for the HTTPs server")
fs.BoolVar(&Config.HTTPRequireAuthToken, "http-require-auth-token", false, "Require auth token to call HTTP APIs")
fs.StringVar(&Config.HTTPAuthPassword, "http-auth-password", "", "Password used to verify auth tokens. Can be changed via API call.")
fs.BoolVar(&Config.APIRequireAuthToken, "api-require-auth", false, "Require authorization token to call HTTP APIs")
fs.StringVar(&Config.APIAuthPassword, "api-auth-password", "", "Password used to create/validate API authorization tokens. Can be changed via API call.")
// Bootstrapping:
bootstrapIPs := fs.String("bootstrap-ips", "default", "Comma separated list of bootstrap peer ips to connect to. Example: 127.0.0.1:9630,127.0.0.1:9631")
@ -409,8 +409,8 @@ func init() {
// HTTP:
Config.HTTPHost = *httpHost
Config.HTTPPort = uint16(*httpPort)
if Config.HTTPRequireAuthToken && Config.HTTPAuthPassword == "" {
errs.Add(errors.New("http-auth-password must be provided if http-require-auth-token is true"))
if Config.APIRequireAuthToken && Config.APIAuthPassword == "" {
errs.Add(errors.New("api-auth-password must be provided if api-require-auth is true"))
return
}

View File

@ -44,13 +44,13 @@ type Config struct {
BootstrapPeers []*Peer
// HTTP configuration
HTTPHost string
HTTPPort uint16
EnableHTTPS bool
HTTPSKeyFile string
HTTPSCertFile string
HTTPRequireAuthToken bool
HTTPAuthPassword string
HTTPHost string
HTTPPort uint16
EnableHTTPS bool
HTTPSKeyFile string
HTTPSCertFile string
APIRequireAuthToken bool
APIAuthPassword string
// Enable/Disable APIs
AdminAPIEnabled bool

View File

@ -393,7 +393,7 @@ func (n *Node) initChains() error {
func (n *Node) initAPIServer() error {
n.Log.Info("Initializing API server")
if err := n.APIServer.Initialize(n.Log, n.LogFactory, n.Config.HTTPHost, n.Config.HTTPPort, n.Config.HTTPRequireAuthToken, n.Config.HTTPAuthPassword); err != nil {
if err := n.APIServer.Initialize(n.Log, n.LogFactory, n.Config.HTTPHost, n.Config.HTTPPort, n.Config.APIRequireAuthToken, n.Config.APIAuthPassword); err != nil {
return err
}