409 lines
9.7 KiB
Go
409 lines
9.7 KiB
Go
package backends
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
h "net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type remoteJWTChecker struct {
|
|
userUri string
|
|
superuserUri string
|
|
aclUri string
|
|
userAgent string
|
|
host string
|
|
port string
|
|
hostWhitelist []string
|
|
withTLS bool
|
|
verifyPeer bool
|
|
paramsMode string
|
|
httpMethod string
|
|
responseMode string
|
|
timeout int
|
|
|
|
options tokenOptions
|
|
|
|
client *h.Client
|
|
}
|
|
|
|
type Response struct {
|
|
Ok bool `json:"ok"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
const (
|
|
whitelistMagicForAnyHost = "*"
|
|
)
|
|
|
|
func NewRemoteJWTChecker(authOpts map[string]string, options tokenOptions, version string) (jwtChecker, error) {
|
|
var checker = &remoteJWTChecker{
|
|
withTLS: false,
|
|
verifyPeer: false,
|
|
responseMode: "status",
|
|
paramsMode: "json",
|
|
httpMethod: h.MethodPost,
|
|
options: options,
|
|
}
|
|
|
|
missingOpts := ""
|
|
remoteOk := true
|
|
|
|
if responseMode, ok := authOpts["jwt_response_mode"]; ok {
|
|
if responseMode == "text" || responseMode == "json" {
|
|
checker.responseMode = responseMode
|
|
}
|
|
}
|
|
|
|
if paramsMode, ok := authOpts["jwt_params_mode"]; ok {
|
|
if paramsMode == "form" {
|
|
checker.paramsMode = paramsMode
|
|
}
|
|
}
|
|
|
|
if httpMethod, ok := authOpts["jwt_http_method"]; ok {
|
|
switch httpMethod {
|
|
case h.MethodGet, h.MethodPut:
|
|
checker.httpMethod = httpMethod
|
|
}
|
|
}
|
|
|
|
if userUri, ok := authOpts["jwt_getuser_uri"]; ok {
|
|
checker.userUri = userUri
|
|
} else {
|
|
remoteOk = false
|
|
missingOpts += " jwt_getuser_uri"
|
|
}
|
|
|
|
if superuserUri, ok := authOpts["jwt_superuser_uri"]; ok {
|
|
checker.superuserUri = superuserUri
|
|
}
|
|
|
|
if aclUri, ok := authOpts["jwt_aclcheck_uri"]; ok {
|
|
checker.aclUri = aclUri
|
|
} else {
|
|
remoteOk = false
|
|
missingOpts += " jwt_aclcheck_uri"
|
|
}
|
|
|
|
checker.userAgent = fmt.Sprintf("%s-%s", defaultUserAgent, version)
|
|
if userAgent, ok := authOpts["jwt_user_agent"]; ok {
|
|
checker.userAgent = userAgent
|
|
}
|
|
|
|
if hostname, ok := authOpts["jwt_host"]; ok {
|
|
checker.host = hostname
|
|
} else if options.parseToken {
|
|
checker.host = ""
|
|
} else {
|
|
remoteOk = false
|
|
missingOpts += " jwt_host"
|
|
}
|
|
|
|
if hostWhitelist, ok := authOpts["jwt_host_whitelist"]; ok {
|
|
if hostWhitelist == whitelistMagicForAnyHost {
|
|
log.Warning(
|
|
"Backend host whitelisting is turned off. This is not secure and should not be used in " +
|
|
"the production environment")
|
|
checker.hostWhitelist = append(checker.hostWhitelist, whitelistMagicForAnyHost)
|
|
} else {
|
|
for _, host := range strings.Split(hostWhitelist, ",") {
|
|
strippedHost := strings.TrimSpace(host)
|
|
/* Not-so-strict check if we have a valid value (domain name or ip address with optional
|
|
port) as a part of the host whitelist. TODO: Consider using more robust check, i.e.
|
|
using "govalidator" or similar package instead. */
|
|
if matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9](?:\:[0-9]+)?$`, strippedHost); !matched {
|
|
return nil, errors.Errorf("JWT backend error: bad host %s in jwt_host_whitelist", strippedHost)
|
|
}
|
|
checker.hostWhitelist = append(checker.hostWhitelist, strippedHost)
|
|
}
|
|
}
|
|
} else if checker.host == "" {
|
|
remoteOk = false
|
|
missingOpts += " jwt_host_whitelist"
|
|
}
|
|
|
|
if port, ok := authOpts["jwt_port"]; ok {
|
|
checker.port = port
|
|
} else {
|
|
remoteOk = false
|
|
missingOpts += " jwt_port"
|
|
}
|
|
|
|
if withTLS, ok := authOpts["jwt_with_tls"]; ok && withTLS == "true" {
|
|
checker.withTLS = true
|
|
}
|
|
|
|
if verifyPeer, ok := authOpts["jwt_verify_peer"]; ok && verifyPeer == "true" {
|
|
checker.verifyPeer = true
|
|
}
|
|
|
|
if !remoteOk {
|
|
return nil, errors.Errorf("JWT backend error: missing remote options: %s", missingOpts)
|
|
}
|
|
|
|
checker.timeout = 5
|
|
if timeoutString, ok := authOpts["jwt_http_timeout"]; ok {
|
|
if timeout, err := strconv.Atoi(timeoutString); err == nil {
|
|
checker.timeout = timeout
|
|
} else {
|
|
log.Errorf("unable to parse timeout: %s", err)
|
|
}
|
|
}
|
|
|
|
checker.client = &h.Client{Timeout: time.Duration(checker.timeout) * time.Second}
|
|
|
|
if !checker.verifyPeer {
|
|
tr := &h.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
checker.client.Transport = tr
|
|
}
|
|
|
|
return checker, nil
|
|
}
|
|
|
|
func (o *remoteJWTChecker) GetUser(token string) (bool, error) {
|
|
var dataMap map[string]interface{}
|
|
var urlValues url.Values
|
|
|
|
if o.options.parseToken {
|
|
username, err := getUsernameForToken(o.options, token, o.options.skipUserExpiration)
|
|
|
|
if err != nil {
|
|
log.Printf("jwt remote get user error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
dataMap = map[string]interface{}{
|
|
"username": username,
|
|
}
|
|
|
|
urlValues = url.Values{
|
|
"username": []string{username},
|
|
}
|
|
}
|
|
|
|
return o.jwtRequest(o.userUri, token, dataMap, urlValues)
|
|
}
|
|
|
|
func (o *remoteJWTChecker) GetSuperuser(token string) (bool, error) {
|
|
if o.superuserUri == "" {
|
|
return false, nil
|
|
}
|
|
var dataMap map[string]interface{}
|
|
var urlValues = url.Values{}
|
|
|
|
if o.options.parseToken {
|
|
username, err := getUsernameForToken(o.options, token, o.options.skipUserExpiration)
|
|
|
|
if err != nil {
|
|
log.Printf("jwt remote get superuser error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
dataMap = map[string]interface{}{
|
|
"username": username,
|
|
}
|
|
|
|
urlValues = url.Values{
|
|
"username": []string{username},
|
|
}
|
|
}
|
|
|
|
return o.jwtRequest(o.superuserUri, token, dataMap, urlValues)
|
|
}
|
|
|
|
func (o *remoteJWTChecker) CheckAcl(token, topic, clientid string, acc int32) (bool, error) {
|
|
dataMap := map[string]interface{}{
|
|
"clientid": clientid,
|
|
"topic": topic,
|
|
"acc": acc,
|
|
}
|
|
var urlValues = url.Values{
|
|
"clientid": []string{clientid},
|
|
"topic": []string{topic},
|
|
"acc": []string{strconv.Itoa(int(acc))},
|
|
}
|
|
|
|
if o.options.parseToken {
|
|
username, err := getUsernameForToken(o.options, token, o.options.skipACLExpiration)
|
|
|
|
if err != nil {
|
|
log.Printf("jwt remote check acl error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
dataMap["username"] = username
|
|
|
|
urlValues.Add("username", username)
|
|
}
|
|
|
|
return o.jwtRequest(o.aclUri, token, dataMap, urlValues)
|
|
}
|
|
|
|
func (o *remoteJWTChecker) Halt() {
|
|
// NO-OP
|
|
}
|
|
|
|
func (o *remoteJWTChecker) jwtRequest(uri, token string, dataMap map[string]interface{}, urlValues url.Values) (bool, error) {
|
|
|
|
// Don't do the request if the client is nil.
|
|
if o.client == nil {
|
|
return false, errors.New("jwt http client not initialized")
|
|
}
|
|
|
|
tlsStr := "http://"
|
|
|
|
if o.withTLS {
|
|
tlsStr = "https://"
|
|
}
|
|
|
|
host, err := o.getHost(token)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
fullURI := fmt.Sprintf("%s%s%s", tlsStr, host, uri)
|
|
// If "host" variable already has port set, do not use the value of jwt_port option from config.
|
|
if !strings.Contains(host, ":") && o.port != "" {
|
|
fullURI = fmt.Sprintf("%s%s:%s%s", tlsStr, host, o.port, uri)
|
|
}
|
|
|
|
var resp *h.Response
|
|
var req *h.Request
|
|
|
|
switch o.paramsMode {
|
|
case "json":
|
|
dataJSON, err := json.Marshal(dataMap)
|
|
|
|
if err != nil {
|
|
log.Errorf("marshal error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
contentReader := bytes.NewReader(dataJSON)
|
|
req, err = h.NewRequest(o.httpMethod, fullURI, contentReader)
|
|
|
|
if err != nil {
|
|
log.Errorf("req error: %s", err)
|
|
return false, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", o.userAgent)
|
|
default:
|
|
req, err = h.NewRequest(o.httpMethod, fullURI, strings.NewReader(urlValues.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(urlValues.Encode())))
|
|
req.Header.Set("User-Agent", o.userAgent)
|
|
|
|
if err != nil {
|
|
log.Errorf("req error: %s", err)
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
|
|
resp, err = o.client.Do(req)
|
|
|
|
if err != nil {
|
|
log.Errorf("error: %v", err)
|
|
return false, err
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
log.Errorf("read error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
log.Infof("error code: %d", resp.StatusCode)
|
|
if resp.StatusCode >= 500 {
|
|
err = fmt.Errorf("error code: %d", resp.StatusCode)
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
if o.responseMode == "text" {
|
|
|
|
//For test response, we expect "ok" or an error message.
|
|
if string(body) != "ok" {
|
|
log.Infof("api error: %s", string(body))
|
|
return false, nil
|
|
}
|
|
|
|
} else if o.responseMode == "json" {
|
|
|
|
//For json response, we expect Ok and Error fields.
|
|
response := Response{Ok: false, Error: ""}
|
|
err = json.Unmarshal(body, &response)
|
|
|
|
if err != nil {
|
|
log.Errorf("unmarshal error: %s", err)
|
|
return false, err
|
|
}
|
|
|
|
if !response.Ok {
|
|
log.Infof("api error: %s", response.Error)
|
|
return false, nil
|
|
}
|
|
|
|
}
|
|
|
|
log.Debugf("jwt request approved for %s", token)
|
|
return true, nil
|
|
}
|
|
|
|
func (o *remoteJWTChecker) getHost(token string) (string, error) {
|
|
if o.host != "" {
|
|
return o.host, nil
|
|
}
|
|
|
|
// Actually this should never happen because of configuration sanity check. TODO: consider removing this condition.
|
|
if !o.options.parseToken {
|
|
errorString := "impossible to obtain host for the authorization request - token parsing is turned off"
|
|
return "", errors.New(errorString)
|
|
}
|
|
|
|
iss, err := getIssForToken(o.options, token, o.options.skipUserExpiration)
|
|
if err != nil {
|
|
errorString := fmt.Sprintf("cannot obtain host for the authorization request from token %s: %s", token, err)
|
|
return "", errors.New(errorString)
|
|
}
|
|
|
|
if !o.isHostWhitelisted(iss) {
|
|
errorString := fmt.Sprintf("host %s obtained from host is not whitelisted; rejecting", iss)
|
|
return "", errors.New(errorString)
|
|
}
|
|
|
|
return iss, nil
|
|
}
|
|
|
|
func (o *remoteJWTChecker) isHostWhitelisted(host string) bool {
|
|
if len(o.hostWhitelist) == 1 && o.hostWhitelist[0] == whitelistMagicForAnyHost {
|
|
return true
|
|
}
|
|
|
|
for _, whitelistedHost := range o.hostWhitelist {
|
|
if whitelistedHost == host {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|