Add basic authentication without dependencies

This adds a rudimentary basic auth scheme without including dependencies.
This commit is contained in:
Shane Neuerburg
2016-11-10 16:35:08 -07:00
parent a88a1e75ed
commit 0cfd025a0d
11 changed files with 192 additions and 70 deletions

View File

@@ -532,9 +532,8 @@ func startRpc(server *utils.Server, internalRaterChan,
go server.ServeGOB(cfg.RPCGOBListen)
go server.ServeHTTP(
cfg.HTTPListen,
cfg.HTTPApiUseBasicAuth,
cfg.HTTPApiBasicAuthRealm,
cfg.HTTPApiHtpasswdFile,
cfg.HTTPUseBasicAuth,
cfg.HTTPAuthUsers,
)
}

View File

@@ -200,25 +200,24 @@ type CGRConfig struct {
StorDBCDRSIndexes []string
DBDataEncoding string // The encoding used to store object data in strings: <msgpack|json>
CacheConfig *CacheConfig
RPCJSONListen string // RPC JSON listening address
RPCGOBListen string // RPC GOB listening address
HTTPListen string // HTTP listening address
HTTPApiUseBasicAuth bool // Use basic auth for HTTP API
HTTPApiBasicAuthRealm string // Basic auth realm URL
HTTPApiHtpasswdFile string // Basic auth htpasswd file path
DefaultReqType string // Use this request type if not defined on top
DefaultCategory string // set default type of record
DefaultTenant string // set default tenant
DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB>
Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb>
ConnectTimeout time.Duration // timeout for RPC connection attempts
ReplyTimeout time.Duration // timeout replies if not reaching back
ConnectAttempts int // number of initial connection attempts before giving up
ResponseCacheTTL time.Duration // the life span of a cached response
InternalTtl time.Duration // maximum duration to wait for internal connections before giving up
RoundingDecimals int // Number of decimals to round end prices at
HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate
TpExportPath string // Path towards export folder for offline Tariff Plans
RPCJSONListen string // RPC JSON listening address
RPCGOBListen string // RPC GOB listening address
HTTPListen string // HTTP listening address
HTTPUseBasicAuth bool // Use basic auth for HTTP API
HTTPAuthUsers map[string]string // Basic auth user:password map (base64 passwords)
DefaultReqType string // Use this request type if not defined on top
DefaultCategory string // set default type of record
DefaultTenant string // set default tenant
DefaultTimezone string // default timezone for timestamps where not specified <""|UTC|Local|$IANA_TZ_DB>
Reconnects int // number of recconect attempts in case of connection lost <-1 for infinite | nb>
ConnectTimeout time.Duration // timeout for RPC connection attempts
ReplyTimeout time.Duration // timeout replies if not reaching back
ConnectAttempts int // number of initial connection attempts before giving up
ResponseCacheTTL time.Duration // the life span of a cached response
InternalTtl time.Duration // maximum duration to wait for internal connections before giving up
RoundingDecimals int // Number of decimals to round end prices at
HttpSkipTlsVerify bool // If enabled Http Client will accept any TLS certificate
TpExportPath string // Path towards export folder for offline Tariff Plans
HttpPosterAttempts int
HttpFailedDir string // Directory path where we store failed http requests
MaxCallDuration time.Duration // The maximum call duration (used by responder when querying DerivedCharging) // ToDo: export it in configuration file
@@ -506,7 +505,7 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error {
return err
}
jsnHttpApiCfg, err := jsnCfg.HttpApiJsonCfg()
jsnHttpCfg, err := jsnCfg.HttpJsonCfg()
if err != nil {
return err
}
@@ -787,15 +786,12 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error {
}
}
if jsnHttpApiCfg != nil {
if jsnHttpApiCfg.Use_basic_auth != nil {
self.HTTPApiUseBasicAuth = *jsnHttpApiCfg.Use_basic_auth
if jsnHttpCfg != nil {
if jsnHttpCfg.Use_basic_auth != nil {
self.HTTPUseBasicAuth = *jsnHttpCfg.Use_basic_auth
}
if jsnHttpApiCfg.Basic_auth_realm != nil {
self.HTTPApiBasicAuthRealm = *jsnHttpApiCfg.Basic_auth_realm
}
if jsnHttpApiCfg.Htpasswd_file != nil {
self.HTTPApiHtpasswdFile = *jsnHttpApiCfg.Htpasswd_file
if jsnHttpCfg.Auth_users != nil {
self.HTTPAuthUsers = *jsnHttpCfg.Auth_users
}
}

View File

@@ -72,10 +72,9 @@ const CGRATES_CFG_JSON = `
},
"http_api": { // HTTP API configuration
"http": { // HTTP API configuration
"use_basic_auth": false, // use basic authentication
"basic_auth_realm": "", // basic auth realm URL
"htpasswd_file": "" // basic auth htpasswd file location
"auth_users": {} // basic authentication usernames and base64-encoded passwords (eg: { "username1": "cGFzc3dvcmQ=", "username2": "cGFzc3dvcmQy "})
},

View File

@@ -29,7 +29,7 @@ const (
GENERAL_JSN = "general"
CACHE_JSN = "cache"
LISTEN_JSN = "listen"
HTTP_API_JSN = "http_api"
HTTP_JSN = "http"
TPDB_JSN = "tariffplan_db"
DATADB_JSN = "data_db"
STORDB_JSN = "stor_db"
@@ -119,12 +119,12 @@ func (self CgrJsonCfg) ListenJsonCfg() (*ListenJsonCfg, error) {
return cfg, nil
}
func (self CgrJsonCfg) HttpApiJsonCfg() (*HTTPApiJsonCfg, error) {
rawCfg, hasKey := self[HTTP_API_JSN]
func (self CgrJsonCfg) HttpJsonCfg() (*HTTPJsonCfg, error) {
rawCfg, hasKey := self[HTTP_JSN]
if !hasKey {
return nil, nil
}
cfg := new(HTTPApiJsonCfg)
cfg := new(HTTPJsonCfg)
if err := json.Unmarshal(*rawCfg, cfg); err != nil {
return nil, err
}

View File

@@ -46,11 +46,10 @@ type ListenJsonCfg struct {
Http *string
}
// HTTP API config section
type HTTPApiJsonCfg struct {
Use_basic_auth *bool
Basic_auth_realm *string
Htpasswd_file *string
// HTTP config section
type HTTPJsonCfg struct {
Use_basic_auth *bool
Auth_users *map[string]string
}
// Database config

View File

@@ -51,12 +51,12 @@
// },
// "http_api" { // HTTP API configuration
// "use_basic_auth": false, // use basic authentication
// "basic_auth_realm": "", // basic auth realm URL
// "htpasswd_file": "", // basic auth htpasswd file location
// "http": { // HTTP API configuration
// "use_basic_auth": false, // use basic authentication
// "auth_users": {} // basic authentication usernames and base64-encoded passwords (eg: { "username1": "cGFzc3dvcmQ=", "username2": "cGFzc3dvcmQy "})
// },
// "tariffplan_db": { // database used to store active tariff plan configuration
// "db_type": "redis", // tariffplan_db type: <redis|mongo>
// "db_host": "127.0.0.1", // tariffplan_db host address

19
glide.lock generated
View File

@@ -1,8 +1,6 @@
hash: cfda8a78a96bf7b3b471463d5ddf3330d6ae2089d5ac7c9a31b0f312e6518595
updated: 2016-11-10T08:26:28.162167756-07:00
hash: da953ea34fabe4e21f4dde6344f3b7ab5a75e02122a1c17af5ea434058fc77fb
updated: 2016-09-06T20:33:01.649869367+02:00
imports:
- name: github.com/abbot/go-http-auth
version: efc9484eee77263a11f158ef4f30fcc30298a942
- name: github.com/bit4bit/gami
version: 3a7f98e7efce7ed7f22c2169b666910b8abb15dc
- name: github.com/cenk/hub
@@ -28,17 +26,13 @@ imports:
- internal/parser/findutil
- internal/parser/intfns
- internal/parser/pathexpr
- literals/boollit
- literals/numlit
- literals/strlit
- internal/xconst
- internal/xsort
- tree
- tree/xmltree
- tree/xmltree/xmlbuilder
- tree/xmltree/xmlele
- tree/xmltree/xmlnode
- xconst
- xfn
- xsort
- name: github.com/DisposaBoy/JsonConfigReader
version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4
- name: github.com/fiorix/go-diameter
@@ -82,11 +76,6 @@ imports:
version: 5cd0f2b3b6cca8e3a0a4101821e41a73cb59bed6
subpackages:
- codec
- name: golang.org/x/crypto
version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd
subpackages:
- bcrypt
- blowfish
- name: golang.org/x/net
version: 1358eff22f0dd0c54fc521042cc607f6ff4b531a
subpackages:

View File

@@ -39,5 +39,3 @@ import:
- package: github.com/hashicorp/golang-lru
- package: github.com/cgrates/aringo
- package: github.com/bit4bit/gami
- package: github.com/abbot/go-http-auth
version: ~0.3.0

98
utils/basic_auth.go Normal file
View File

@@ -0,0 +1,98 @@
/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package utils
import (
"encoding/base64"
"fmt"
"net/http"
"strings"
)
// use provides a cleaner interface for chaining middleware for single routes.
// Middleware functions are simple HTTP handlers (w http.ResponseWriter, r *http.Request)
//
// r.HandleFunc("/login", use(loginHandler, rateLimit, csrf))
// r.HandleFunc("/form", use(formHandler, csrf))
// r.HandleFunc("/about", aboutHandler)
//
// From https://gist.github.com/elithrar/9146306
// See https://gist.github.com/elithrar/7600878#comment-955958 for how to extend it to suit simple http.Handler's
func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
for _, m := range middleware {
h = m(h)
}
return h
}
type basicAuthMiddleware func(h http.HandlerFunc) http.HandlerFunc
// basicAuth returns a middleware function to intercept the request and validate
func basicAuth(userList map[string]string) basicAuthMiddleware {
return func(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
authHeader := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(authHeader) != 2 {
Logger.Warning("<BasicAuth> Missing authorization header value")
http.Error(w, "Not authorized", 401)
return
}
authHeaderDecoded, err := base64.StdEncoding.DecodeString(authHeader[1])
if err != nil {
Logger.Warning("<BasicAuth> Unable to decode authorization header")
http.Error(w, err.Error(), 401)
return
}
userPass := strings.SplitN(string(authHeaderDecoded), ":", 2)
if len(userPass) != 2 {
Logger.Warning("<BasicAuth> Unauthorized API access. Missing or extra credential components")
http.Error(w, "Not authorized", 401)
return
}
valid := verifyCredential(userPass[0], userPass[1], userList)
if !valid {
Logger.Warning(fmt.Sprintf("<BasicAuth> Unauthorized API access by user '%s'", userPass[0]))
http.Error(w, "Not authorized", 401)
return
}
h.ServeHTTP(w, r)
}
}
}
// verifyCredential validates the incoming username and password against the authorized user list
func verifyCredential(username string, password string, userList map[string]string) bool {
hash, ok := userList[username]
if !ok {
return false
}
storedPass, err := base64.StdEncoding.DecodeString(hash)
if err != nil {
return false
}
return string(storedPass[:]) == password
}

47
utils/basic_auth_test.go Normal file
View File

@@ -0,0 +1,47 @@
/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package utils
import "testing"
func TestVerifyCredential(t *testing.T) {
var hashedPasswords = map[string]string{
"1234": "MTIzNA==",
"bar": "YmFy",
}
var verifyCredentialTests = []struct {
username string
password string
userList map[string]string
result bool
}{
{"test", "1234", map[string]string{"test": hashedPasswords["1234"]}, true},
{"test", "0000", map[string]string{"test": hashedPasswords["1234"]}, false},
{"foo", "bar", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, true},
{"foo", "1234", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, false},
{"none", "1234", map[string]string{"test": "1234", "foo": hashedPasswords["bar"]}, false},
}
for _, tt := range verifyCredentialTests {
r := verifyCredential(tt.username, tt.password, tt.userList)
if r != tt.result {
t.Errorf("verifyCredential(%s, %s, %v) => %t, want %t", tt.username, tt.password, tt.userList, r, tt.result)
}
}
}

View File

@@ -29,7 +29,6 @@ import (
"reflect"
"time"
"github.com/abbot/go-http-auth"
"github.com/cenk/rpc2"
)
import _ "net/http/pprof"
@@ -147,13 +146,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
io.Copy(w, res)
}
func (s *Server) ServeHTTP(addr string, useBasicAuth bool, basicAuthRealm string, htpasswdFile string) {
func (s *Server) ServeHTTP(addr string, useBasicAuth bool, userList map[string]string) {
if s.rpcEnabled {
if useBasicAuth {
Logger.Info(fmt.Sprintf("Configuring CGRateS HTTP server to use basic auth (realm: %s, htpasswd: %s).", basicAuthRealm, htpasswdFile))
secrets := auth.HtpasswdFileProvider(htpasswdFile)
authenticator := auth.NewBasicAuthenticator(basicAuthRealm, secrets)
http.HandleFunc("/jsonrpc", auth.JustCheck(authenticator, handleRequest))
Logger.Info("Configuring CGRateS HTTP server to use basic auth")
http.HandleFunc("/jsonrpc", use(handleRequest, basicAuth(userList)))
} else {
http.HandleFunc("/jsonrpc", handleRequest)
}