Compare commits

..

No commits in common. "oauth" and "master" have entirely different histories.

7 changed files with 75 additions and 280 deletions

View File

@ -9,7 +9,6 @@ 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
@ -53,9 +52,6 @@ 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)

View File

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

View File

@ -42,6 +42,8 @@ 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]"
@ -70,24 +72,52 @@ 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 { if err != nil || timeout <= 0 {
timeout = 0 timeout = TimeoutSec
}
if timeout > MaxTimeout {
timeout = MaxTimeout
} }
answer := xmpp.Confirm(jid, method, domain, transaction, timeout) chanAnswer := make(chan string)
switch answer {
case xmpp.REPLY_OK:
w.WriteHeader(http.StatusOK)
case xmpp.REPLY_DENY: confirmation := new(xmpp.Confirmation)
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)
}
case xmpp.REPLY_UNREACHABLE: switch confirmation.TypeSend {
w.WriteHeader(StatusUnreachable) case xmpp.TYPE_SEND_IQ:
log.Printf("%sDelete IQ", LogDebug)
delete(xmpp.WaitIqMessages, confirmation.IdMap)
default: case xmpp.TYPE_SEND_MESSAGE:
w.WriteHeader(StatusUnknownError) log.Printf("%sDelete Message", LogDebug)
delete(xmpp.WaitMessageAnswers, confirmation.IdMap)
} }
} }
@ -97,10 +127,6 @@ 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" {

View File

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

View File

@ -1,18 +0,0 @@
<!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,14 +6,12 @@ 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"
@ -26,9 +24,6 @@ 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 {
@ -43,46 +38,6 @@ 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)