Compare commits

..

5 Commits

Author SHA1 Message Date
chteufleur 600117e9b2 Merge branch 'master' into oauth
Conflicts:
	xmpp/confirmation.go
2017-05-17 20:09:52 +02:00
Chteufleur 94936bdc3a Fix typo in go_xmpp lib. 2017-03-21 22:24:16 +01:00
Chteufleur 3e8cec5bd9 Add README section about client store configuration. 2017-03-20 22:22:50 +01:00
Chteufleur 850a283083 First OAuth implementation. 2017-03-20 22:12:49 +01:00
Chteufleur 558f9d6029 Reorganize code to be reusable. 2017-03-17 23:43:20 +01:00
7 changed files with 280 additions and 75 deletions

View File

@ -9,6 +9,7 @@ Can be run as a XMPP client or XMPP component.
* [go-xmpp](https://git.kingpenguin.tk/chteufleur/go-xmpp) for the XMPP part. * [go-xmpp](https://git.kingpenguin.tk/chteufleur/go-xmpp) for the XMPP part.
* [cfg](https://github.com/jimlawless/cfg) for the configuration file. * [cfg](https://github.com/jimlawless/cfg) for the configuration file.
* [oauth2.v3](https://github.com/go-oauth2/oauth2) for OAuth2 support.
### Build and run ### Build and run
@ -52,6 +53,9 @@ If ``http_bind_address_ipv4`` is set to ``0.0.0.0``, it will bind all address on
The lang messages file must be placed into the same directory than the configuration file. The lang messages file must be placed into the same directory than the configuration file.
An example of this file can be found in [the repos](https://git.kingpenguin.tk/chteufleur/HTTPAuthentificationOverXMPP/src/master/messages.lang) An example of this file can be found in [the repos](https://git.kingpenguin.tk/chteufleur/HTTPAuthentificationOverXMPP/src/master/messages.lang)
### OAuth configuration
OAuth configuration is made by a file that can be found in [the repos](https://git.kingpenguin.tk/chteufleur/HTTPAuthentificationOverXMPP/src/oauth/clientStore.json) and must be place following [XDG specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) (example ``/etc/xdg/http-auth/clientStore.json``).
### Usage ### Usage
To ask authorization, just send an HTTP request to the path ``/auth`` with parameters: To ask authorization, just send an HTTP request to the path ``/auth`` with parameters:
* __jid__ : JID of the user (user@host/resource or user@host) * __jid__ : JID of the user (user@host/resource or user@host)

7
clientStore.json Normal file
View File

@ -0,0 +1,7 @@
[
{
"ID": "key",
"Secret": "secret",
"Domain": "http://localhost:9094"
}
]

View File

@ -42,8 +42,6 @@ var (
KeyPath = "./key.pem" KeyPath = "./key.pem"
ChanRequest = make(chan interface{}, 5) ChanRequest = make(chan interface{}, 5)
TimeoutSec = 60 // 1 min
MaxTimeout = 300 // 5 min
BindAddressIPv4 = "127.0.0.1" BindAddressIPv4 = "127.0.0.1"
BindAddressIPv6 = "[::1]" BindAddressIPv6 = "[::1]"
@ -72,52 +70,24 @@ func authHandler(w http.ResponseWriter, r *http.Request) {
} }
timeoutStr := strings.Join(r.Form[TIMEOUTE], "") timeoutStr := strings.Join(r.Form[TIMEOUTE], "")
log.Printf("%sAuth %s", LogInfo, jid)
timeout, err := strconv.Atoi(timeoutStr) timeout, err := strconv.Atoi(timeoutStr)
if err != nil || timeout <= 0 { if err != nil {
timeout = TimeoutSec timeout = 0
}
if timeout > MaxTimeout {
timeout = MaxTimeout
} }
chanAnswer := make(chan string) answer := xmpp.Confirm(jid, method, domain, transaction, timeout)
switch answer {
case xmpp.REPLY_OK:
w.WriteHeader(http.StatusOK)
confirmation := new(xmpp.Confirmation) case xmpp.REPLY_DENY:
confirmation.JID = jid
confirmation.Method = method
confirmation.Domain = domain
confirmation.Transaction = transaction
confirmation.ChanReply = chanAnswer
confirmation.SendConfirmation()
select {
case answer := <-chanAnswer:
switch answer {
case xmpp.REPLY_OK:
w.WriteHeader(http.StatusOK)
case xmpp.REPLY_DENY:
w.WriteHeader(http.StatusUnauthorized)
case xmpp.REPLY_UNREACHABLE:
w.WriteHeader(StatusUnreachable)
default:
w.WriteHeader(StatusUnknownError)
}
case <-time.After(time.Duration(timeout) * time.Second):
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
}
switch confirmation.TypeSend { case xmpp.REPLY_UNREACHABLE:
case xmpp.TYPE_SEND_IQ: w.WriteHeader(StatusUnreachable)
log.Printf("%sDelete IQ", LogDebug)
delete(xmpp.WaitIqMessages, confirmation.IdMap)
case xmpp.TYPE_SEND_MESSAGE: default:
log.Printf("%sDelete Message", LogDebug) w.WriteHeader(StatusUnknownError)
delete(xmpp.WaitMessageAnswers, confirmation.IdMap)
} }
} }
@ -127,6 +97,10 @@ func Run() {
http.HandleFunc(ROUTE_ROOT, indexHandler) http.HandleFunc(ROUTE_ROOT, indexHandler)
http.HandleFunc(ROUTE_AUTH, authHandler) http.HandleFunc(ROUTE_AUTH, authHandler)
// OAuth2
LoadOAuth()
LoadServer()
if HttpPortBind > 0 { if HttpPortBind > 0 {
go runHttp(BindAddressIPv4) go runHttp(BindAddressIPv4)
if BindAddressIPv4 != "0.0.0.0" { if BindAddressIPv4 != "0.0.0.0" {

148
http/oauth.go Normal file
View File

@ -0,0 +1,148 @@
package http
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"git.kingpenguin.tk/chteufleur/HTTPAuthentificationOverXMPP.git/xmpp"
"gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/models"
"gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store"
"gopkg.in/session.v1"
)
const (
ROUTE_LOGIN = "/login"
ROUTE_AUTHORIZE = "/authorize"
ROUTE_TOKEN = "/token"
)
var (
globalSessions *session.Manager
manager *manage.Manager
clientList []models.Client
)
func init() {
globalSessions, _ = session.NewManager("memory", `{"cookieName":"gosessionid","gclifetime":3600}`)
go globalSessions.GC()
}
func LoadConfigClient(configFile string) error {
file, err := ioutil.ReadFile(configFile)
if err != nil {
return err
}
return json.Unmarshal(file, &clientList)
}
func LoadOAuth() {
manager = manage.NewDefaultManager()
// token store
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
for _, c := range clientList {
clientStore.Set(c.ID, &c)
}
manager.MapClientStorage(clientStore)
}
func LoadServer() {
srv := server.NewServer(server.NewConfig(), manager)
srv.SetUserAuthorizationHandler(userAuthorizeHandler)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc(ROUTE_LOGIN, loginHandler)
http.HandleFunc(ROUTE_AUTHORIZE, func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
http.HandleFunc(ROUTE_TOKEN, func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
}
func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
us, err := globalSessions.SessionStart(w, r)
uid := us.Get("UserID")
if uid == nil {
if r.Form == nil {
r.ParseForm()
}
us.Set("Form", r.Form)
w.Header().Set("Location", ROUTE_LOGIN)
w.WriteHeader(http.StatusFound)
return
}
userID = uid.(string)
us.Delete("UserID")
return
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
us, err := globalSessions.SessionStart(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm()
jid := strings.Join(r.Form["jid"], "")
// TODO
answer := xmpp.Confirm(jid, "GET", "toto.fr", "oauth2", -1)
switch answer {
case xmpp.REPLY_OK:
us.Set("LoggedInUserID", jid)
form := us.Get("Form").(url.Values)
u := new(url.URL)
u.Path = ROUTE_AUTHORIZE
u.RawQuery = form.Encode()
w.Header().Set("Location", u.String())
w.WriteHeader(http.StatusFound)
us.Delete("Form")
us.Set("UserID", us.Get("LoggedInUserID"))
case xmpp.REPLY_DENY:
w.WriteHeader(http.StatusUnauthorized)
case xmpp.REPLY_UNREACHABLE:
w.WriteHeader(StatusUnreachable)
default:
w.WriteHeader(StatusUnknownError)
}
return
}
outputHTML(w, r, "static/login.html")
}
func outputHTML(w http.ResponseWriter, req *http.Request, filename string) {
file, err := os.Open(filename)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer file.Close()
fi, _ := file.Stat()
http.ServeContent(w, req, file.Name(), fi.ModTime(), file)
}

77
main.go
View File

@ -16,11 +16,13 @@ import (
) )
const ( const (
Version = "v0.5-dev" Version = "v0.5-dev"
configurationFilePath = "http-auth/httpAuth.conf" configurationDirectory = "http-auth"
langFilePath = "http-auth/messages.lang" configurationFilePath = "httpAuth.conf"
PathConfEnvVariable = "XDG_CONFIG_DIRS" langFilePath = "messages.lang"
DefaultXdgConfigDirs = "/etc/xdg" oauthClientStoreFilePath = "clientStore.json"
PathConfEnvVariable = "XDG_CONFIG_DIRS"
DefaultXdgConfigDirs = "/etc/xdg"
) )
var ( var (
@ -34,12 +36,13 @@ func init() {
log.Fatal("Failed to load configuration file.") log.Fatal("Failed to load configuration file.")
} }
loadLangFile() loadLangFile()
loadOauthClientStore()
// HTTP config // HTTP config
httpTimeout, err := strconv.Atoi(mapConfig["http_timeout_sec"]) httpTimeout, err := strconv.Atoi(mapConfig["http_timeout_sec"])
if err == nil && httpTimeout > 0 && httpTimeout < http.MaxTimeout { if err == nil && httpTimeout > 0 && httpTimeout < xmpp.MaxTimeout {
log.Println("Define HTTP timeout to " + strconv.Itoa(httpTimeout) + " second") log.Println("Define HTTP timeout to " + strconv.Itoa(httpTimeout) + " second")
http.TimeoutSec = httpTimeout xmpp.TimeoutSec = httpTimeout
} }
httpPort, err := strconv.Atoi(mapConfig["http_port"]) httpPort, err := strconv.Atoi(mapConfig["http_port"])
if err == nil { if err == nil {
@ -78,50 +81,56 @@ func init() {
xmpp.VerifyCertValidity = mapConfig["xmpp_verify_cert_validity"] != "false" // Default TRUE xmpp.VerifyCertValidity = mapConfig["xmpp_verify_cert_validity"] != "false" // Default TRUE
} }
func loadConfigFile() bool { func loadConfigurationFile(file string) string {
ret := false ret := ""
envVariable := os.Getenv(PathConfEnvVariable) envVariable := os.Getenv(PathConfEnvVariable)
if envVariable == "" { if envVariable == "" {
envVariable = DefaultXdgConfigDirs envVariable = DefaultXdgConfigDirs
} }
for _, path := range strings.Split(envVariable, ":") { for _, path := range strings.Split(envVariable, ":") {
log.Println("Try to find configuration file into " + path) log.Println("Try to find file (" + file + ") into " + path)
configFile := path + "/" + configurationFilePath configFile := path + "/" + configurationDirectory + "/" + file
if _, err := os.Stat(configFile); err == nil { if _, err := os.Stat(configFile); err == nil {
// The config file exist // The file exist
if cfg.Load(configFile, mapConfig) == nil { ret = configFile
// And has been loaded succesfully break
log.Println("Find configuration file at " + configFile)
ret = true
break
}
} }
} }
return ret return ret
} }
func loadConfigFile() bool {
ret := false
configFile := loadConfigurationFile(configurationFilePath)
if configFile != "" && cfg.Load(configFile, mapConfig) == nil {
// And has been loaded succesfully
log.Println("Find configuration file at " + configFile)
ret = true
}
return ret
}
func loadLangFile() bool { func loadLangFile() bool {
ret := false ret := false
envVariable := os.Getenv(PathConfEnvVariable) langFile := loadConfigurationFile(langFilePath)
if envVariable == "" { if cfg.Load(langFile, xmpp.MapLangs) == nil {
envVariable = DefaultXdgConfigDirs // And has been loaded succesfully
} log.Println("Find messages lang file at " + langFile)
for _, path := range strings.Split(envVariable, ":") { ret = true
log.Println("Try to find messages lang file into " + path)
langFile := path + "/" + langFilePath
if _, err := os.Stat(langFile); err == nil {
// The config file exist
if cfg.Load(langFile, xmpp.MapLangs) == nil {
// And has been loaded succesfully
log.Println("Find messages lang file at " + langFile)
ret = true
break
}
}
} }
return ret return ret
} }
func loadOauthClientStore() {
clientStore := loadConfigurationFile(oauthClientStoreFilePath)
err := http.LoadConfigClient(clientStore)
if err == nil {
log.Printf("Find OAuth client store file at " + clientStore)
} else {
log.Printf("Failed to load OAuth client store: ", err)
}
}
func main() { func main() {
go http.Run() go http.Run()

18
static/login.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>HTTP authentication over XMPP</title>
</head>
<body>
<div class="container">
<h1>HTTP authentication over XMPP</h1>
<form action="/login" method="POST">
<label for="username">Jabber ID</label>
<input type="text" class="form-control" name="jid" placeholder="Please enter your JID">
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

View File

@ -6,12 +6,14 @@ import (
"log" "log"
"strconv" "strconv"
"strings" "strings"
"time"
) )
const ( const (
REPLY_UNREACHABLE = "reply_unreachable" REPLY_UNREACHABLE = "reply_unreachable"
REPLY_DENY = "reply_deny" REPLY_DENY = "reply_deny"
REPLY_OK = "reply_ok" REPLY_OK = "reply_ok"
REPLY_TIMEOUT = "reply_timeout"
TYPE_SEND_MESSAGE = "type_send_message" TYPE_SEND_MESSAGE = "type_send_message"
TYPE_SEND_IQ = "type_send_iq" TYPE_SEND_IQ = "type_send_iq"
@ -24,6 +26,9 @@ const (
var ( var (
MapLangs = make(map[string]string) MapLangs = make(map[string]string)
TimeoutSec = 60 // 1 min
MaxTimeout = 300 // 5 min
) )
type Confirmation struct { type Confirmation struct {
@ -38,6 +43,46 @@ type Confirmation struct {
ChanReply chan string ChanReply chan string
} }
func Confirm(jid, method, domain, transaction string, timeout int) string {
// TODO check param validity
ret := ""
log.Printf("%sAuth %s", LogInfo, jid)
chanAnswer := make(chan string)
if timeout <= 0 {
timeout = TimeoutSec
}
if timeout > MaxTimeout {
timeout = MaxTimeout
}
confirmation := new(Confirmation)
confirmation.JID = jid
confirmation.Method = method
confirmation.Domain = domain
confirmation.Transaction = transaction
confirmation.ChanReply = chanAnswer
confirmation.SendConfirmation()
select {
case answer := <-chanAnswer:
ret = answer
case <-time.After(time.Duration(timeout) * time.Second):
ret = REPLY_TIMEOUT
}
switch confirmation.TypeSend {
case TYPE_SEND_IQ:
log.Printf("%sDelete IQ", LogDebug)
delete(WaitIqMessages, confirmation.IdMap)
case TYPE_SEND_MESSAGE:
log.Printf("%sDelete Message", LogDebug)
delete(WaitMessageAnswers, confirmation.IdMap)
}
return ret
}
func (confirmation *Confirmation) SendConfirmation() { func (confirmation *Confirmation) SendConfirmation() {
log.Printf("%sQuery JID %s", LogInfo, confirmation.JID) log.Printf("%sQuery JID %s", LogInfo, confirmation.JID)
clientJID, _ := xmpp.ParseJID(confirmation.JID) clientJID, _ := xmpp.ParseJID(confirmation.JID)