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.
* [cfg](https://github.com/jimlawless/cfg) for the configuration file.
* [oauth2.v3](https://github.com/go-oauth2/oauth2) for OAuth2 support.
### 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.
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
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)

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"
ChanRequest = make(chan interface{}, 5)
TimeoutSec = 60 // 1 min
MaxTimeout = 300 // 5 min
BindAddressIPv4 = "127.0.0.1"
BindAddressIPv6 = "[::1]"
@ -72,52 +70,24 @@ func authHandler(w http.ResponseWriter, r *http.Request) {
}
timeoutStr := strings.Join(r.Form[TIMEOUTE], "")
log.Printf("%sAuth %s", LogInfo, jid)
timeout, err := strconv.Atoi(timeoutStr)
if err != nil || timeout <= 0 {
timeout = TimeoutSec
}
if timeout > MaxTimeout {
timeout = MaxTimeout
if err != nil {
timeout = 0
}
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)
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):
case xmpp.REPLY_DENY:
w.WriteHeader(http.StatusUnauthorized)
}
switch confirmation.TypeSend {
case xmpp.TYPE_SEND_IQ:
log.Printf("%sDelete IQ", LogDebug)
delete(xmpp.WaitIqMessages, confirmation.IdMap)
case xmpp.REPLY_UNREACHABLE:
w.WriteHeader(StatusUnreachable)
case xmpp.TYPE_SEND_MESSAGE:
log.Printf("%sDelete Message", LogDebug)
delete(xmpp.WaitMessageAnswers, confirmation.IdMap)
default:
w.WriteHeader(StatusUnknownError)
}
}
@ -127,6 +97,10 @@ func Run() {
http.HandleFunc(ROUTE_ROOT, indexHandler)
http.HandleFunc(ROUTE_AUTH, authHandler)
// OAuth2
LoadOAuth()
LoadServer()
if HttpPortBind > 0 {
go runHttp(BindAddressIPv4)
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 (
Version = "v0.5-dev"
configurationFilePath = "http-auth/httpAuth.conf"
langFilePath = "http-auth/messages.lang"
PathConfEnvVariable = "XDG_CONFIG_DIRS"
DefaultXdgConfigDirs = "/etc/xdg"
Version = "v0.5-dev"
configurationDirectory = "http-auth"
configurationFilePath = "httpAuth.conf"
langFilePath = "messages.lang"
oauthClientStoreFilePath = "clientStore.json"
PathConfEnvVariable = "XDG_CONFIG_DIRS"
DefaultXdgConfigDirs = "/etc/xdg"
)
var (
@ -34,12 +36,13 @@ func init() {
log.Fatal("Failed to load configuration file.")
}
loadLangFile()
loadOauthClientStore()
// HTTP config
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")
http.TimeoutSec = httpTimeout
xmpp.TimeoutSec = httpTimeout
}
httpPort, err := strconv.Atoi(mapConfig["http_port"])
if err == nil {
@ -78,50 +81,56 @@ func init() {
xmpp.VerifyCertValidity = mapConfig["xmpp_verify_cert_validity"] != "false" // Default TRUE
}
func loadConfigFile() bool {
ret := false
func loadConfigurationFile(file string) string {
ret := ""
envVariable := os.Getenv(PathConfEnvVariable)
if envVariable == "" {
envVariable = DefaultXdgConfigDirs
}
for _, path := range strings.Split(envVariable, ":") {
log.Println("Try to find configuration file into " + path)
configFile := path + "/" + configurationFilePath
log.Println("Try to find file (" + file + ") into " + path)
configFile := path + "/" + configurationDirectory + "/" + file
if _, err := os.Stat(configFile); err == nil {
// The config file exist
if cfg.Load(configFile, mapConfig) == nil {
// And has been loaded succesfully
log.Println("Find configuration file at " + configFile)
ret = true
break
}
// The file exist
ret = configFile
break
}
}
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 {
ret := false
envVariable := os.Getenv(PathConfEnvVariable)
if envVariable == "" {
envVariable = DefaultXdgConfigDirs
}
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
}
}
langFile := loadConfigurationFile(langFilePath)
if cfg.Load(langFile, xmpp.MapLangs) == nil {
// And has been loaded succesfully
log.Println("Find messages lang file at " + langFile)
ret = true
}
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() {
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"
"strconv"
"strings"
"time"
)
const (
REPLY_UNREACHABLE = "reply_unreachable"
REPLY_DENY = "reply_deny"
REPLY_OK = "reply_ok"
REPLY_TIMEOUT = "reply_timeout"
TYPE_SEND_MESSAGE = "type_send_message"
TYPE_SEND_IQ = "type_send_iq"
@ -24,6 +26,9 @@ const (
var (
MapLangs = make(map[string]string)
TimeoutSec = 60 // 1 min
MaxTimeout = 300 // 5 min
)
type Confirmation struct {
@ -38,6 +43,46 @@ type Confirmation struct {
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() {
log.Printf("%sQuery JID %s", LogInfo, confirmation.JID)
clientJID, _ := xmpp.ParseJID(confirmation.JID)