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"
|
||||
)
|
||||
|
||||
// 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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue