mirror of https://github.com/poanetwork/gecko.git
authorize on a per-endpoint basis. Change name of CLIs
This commit is contained in:
parent
0d7b224e5f
commit
2640977cde
|
@ -13,8 +13,6 @@ import (
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: Add method to revoke a token
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
headerKey = "Authorization"
|
headerKey = "Authorization"
|
||||||
headerValStart = "Bearer "
|
headerValStart = "Bearer "
|
||||||
|
@ -41,6 +39,15 @@ type Auth struct {
|
||||||
revoked []string // List of tokens that have been revoked
|
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
|
// getToken gets the JWT token from the request header
|
||||||
// Assumes the header is this form:
|
// Assumes the header is this form:
|
||||||
// "Authorization": "Bearer TOKEN.GOES.HERE"
|
// "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
|
return rawHeader[len(headerValStart):], nil // Returns actual auth token. Slice guaranteed to not go OOB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and return a new token
|
// Create and return a new token that allows access to each API endpoint such
|
||||||
func (auth *Auth) newToken(password string) (string, error) {
|
// 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()
|
auth.lock.RLock()
|
||||||
defer auth.lock.RUnlock()
|
defer auth.lock.RUnlock()
|
||||||
if password != auth.Password {
|
if password != auth.Password {
|
||||||
return "", errWrongPassword
|
return "", errWrongPassword
|
||||||
}
|
}
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ // Make a new token
|
canAccessAll := false
|
||||||
ExpiresAt: time.Now().Add(TokenLifespan).Unix(),
|
for _, endpoint := range endpoints {
|
||||||
})
|
if endpoint == "*" {
|
||||||
return token.SignedString([]byte(auth.Password)) // Sign it
|
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 {
|
if auth.Password != oldPassword {
|
||||||
return errWrongPassword
|
return errWrongPassword
|
||||||
} else if len(newPassword) == 0 || len(newPassword) > maxPasswordLen {
|
} 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 {
|
} else if oldPassword == newPassword {
|
||||||
return errors.New("new password can't be same as old password")
|
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
|
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()
|
auth.lock.RLock()
|
||||||
defer auth.lock.RUnlock()
|
defer auth.lock.RUnlock()
|
||||||
return []byte(auth.Password), nil
|
return []byte(auth.Password), nil
|
||||||
})
|
})
|
||||||
if err != nil { // Signature is probably wrong
|
if err != nil { // Probably because signature wrong
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
io.WriteString(w, fmt.Sprintf("invalid auth token: %s", err))
|
io.WriteString(w, fmt.Sprintf("invalid auth token: %s", err))
|
||||||
return
|
return
|
||||||
|
@ -148,14 +172,43 @@ func (auth *Auth) WrapHandler(h http.Handler) http.Handler {
|
||||||
io.WriteString(w, "invalid auth token. Is it expired?")
|
io.WriteString(w, "invalid auth token. Is it expired?")
|
||||||
return
|
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
|
for _, revokedToken := range auth.revoked { // Make sure this token wasn't revoked
|
||||||
if revokedToken == tokenStr {
|
if revokedToken == tokenStr {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
io.WriteString(w, "this token was revoked")
|
io.WriteString(w, "the provided auth token was revoked")
|
||||||
|
auth.lock.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
auth.lock.RUnlock()
|
||||||
|
|
||||||
h.ServeHTTP(w, r) // Authentication successful
|
h.ServeHTTP(w, r) // Authorization successful
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,10 @@ import (
|
||||||
"github.com/ava-labs/gecko/utils/logging"
|
"github.com/ava-labs/gecko/utils/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxEndpoints = 128
|
||||||
|
)
|
||||||
|
|
||||||
// Service ...
|
// Service ...
|
||||||
type Service struct {
|
type Service struct {
|
||||||
log logging.Logger
|
log logging.Logger
|
||||||
|
@ -37,18 +41,33 @@ type Password struct {
|
||||||
Password string `json:"password"` // The authotization password
|
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 ...
|
// Token ...
|
||||||
type Token struct {
|
type Token struct {
|
||||||
Token string `json:"token"` // The new token. Expires in [TokenLifespan].
|
Token string `json:"token"` // The new token. Expires in [TokenLifespan].
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewToken returns a new token
|
// 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")
|
s.log.Info("Auth: NewToken called")
|
||||||
if args.Password == "" {
|
if args.Password.Password == "" {
|
||||||
return fmt.Errorf("password not given")
|
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
|
reply.Token = token
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,8 +197,8 @@ func init() {
|
||||||
fs.BoolVar(&Config.EnableHTTPS, "http-tls-enabled", false, "Upgrade the HTTP server to HTTPs")
|
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.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.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.BoolVar(&Config.APIRequireAuthToken, "api-require-auth", false, "Require authorization 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.StringVar(&Config.APIAuthPassword, "api-auth-password", "", "Password used to create/validate API authorization tokens. Can be changed via API call.")
|
||||||
|
|
||||||
// Bootstrapping:
|
// 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")
|
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:
|
// HTTP:
|
||||||
Config.HTTPHost = *httpHost
|
Config.HTTPHost = *httpHost
|
||||||
Config.HTTPPort = uint16(*httpPort)
|
Config.HTTPPort = uint16(*httpPort)
|
||||||
if Config.HTTPRequireAuthToken && Config.HTTPAuthPassword == "" {
|
if Config.APIRequireAuthToken && Config.APIAuthPassword == "" {
|
||||||
errs.Add(errors.New("http-auth-password must be provided if http-require-auth-token is true"))
|
errs.Add(errors.New("api-auth-password must be provided if api-require-auth is true"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,13 +44,13 @@ type Config struct {
|
||||||
BootstrapPeers []*Peer
|
BootstrapPeers []*Peer
|
||||||
|
|
||||||
// HTTP configuration
|
// HTTP configuration
|
||||||
HTTPHost string
|
HTTPHost string
|
||||||
HTTPPort uint16
|
HTTPPort uint16
|
||||||
EnableHTTPS bool
|
EnableHTTPS bool
|
||||||
HTTPSKeyFile string
|
HTTPSKeyFile string
|
||||||
HTTPSCertFile string
|
HTTPSCertFile string
|
||||||
HTTPRequireAuthToken bool
|
APIRequireAuthToken bool
|
||||||
HTTPAuthPassword string
|
APIAuthPassword string
|
||||||
|
|
||||||
// Enable/Disable APIs
|
// Enable/Disable APIs
|
||||||
AdminAPIEnabled bool
|
AdminAPIEnabled bool
|
||||||
|
|
|
@ -393,7 +393,7 @@ func (n *Node) initChains() error {
|
||||||
func (n *Node) initAPIServer() error {
|
func (n *Node) initAPIServer() error {
|
||||||
n.Log.Info("Initializing API server")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue