Initial commit.
This commit is contained in:
commit
5cffe2b8ae
820
main.go
Normal file
820
main.go
Normal file
@ -0,0 +1,820 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base32"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/badoux/checkmail"
|
||||||
|
_ "github.com/mattn/go-sqlite3" // MIT licensed.
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/gomail.v2"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/fcgi"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB related stuff
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
token string
|
||||||
|
cookie string
|
||||||
|
created int64
|
||||||
|
expiry int64
|
||||||
|
request []byte
|
||||||
|
confirmed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
server_id string
|
||||||
|
created int64
|
||||||
|
email string
|
||||||
|
data string
|
||||||
|
ip string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
email string
|
||||||
|
created int64
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
email string
|
||||||
|
name string
|
||||||
|
server_id string
|
||||||
|
created int64
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON data blobs
|
||||||
|
type Player_data struct {
|
||||||
|
Auth_required string `json:"auth_required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON interface to WWW, and gameserver
|
||||||
|
type Serverdata struct {
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Announce string `json:"announce"`
|
||||||
|
AnnounceUrl string `json:"announce_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tfa_request struct {
|
||||||
|
// required
|
||||||
|
Request_type string `json:"request_type"`
|
||||||
|
Remote_ip string `json:"remote_ip"`
|
||||||
|
// optional, need to verify they're present at a later stage
|
||||||
|
Email string `json:"email",omitempty`
|
||||||
|
Player string `json:"player",omitempty`
|
||||||
|
Server_id string `json:"server_id",omitempty`
|
||||||
|
Token string `json:"token",omitmempty`
|
||||||
|
Cookie string `json:"cookie",omitempty`
|
||||||
|
Serverdata Serverdata `json:"server_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tfa_response struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
// optional
|
||||||
|
Data map[string]string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// misc functions
|
||||||
|
func make_token() string {
|
||||||
|
b := make([]byte, 24)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error creating token: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := base32.StdEncoding.EncodeToString(b)
|
||||||
|
|
||||||
|
return s[:len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate_email(email string) bool {
|
||||||
|
err := checkmail.ValidateFormat(email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("email: %v: %v\n", email, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
//FIXME enable this when not running behind a NAT
|
||||||
|
//err = checkmail.ValidateHost(email)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Printf("email: %v: %v\n", email, err)
|
||||||
|
//}
|
||||||
|
//if smtpErr, ok := err.(checkmail.SmtpError); ok && err != nil {
|
||||||
|
// log.Printf("email: %v: code: %s, msg: %s", email, smtpErr.Code(), smtpErr)
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func do_email(email string, message string) bool {
|
||||||
|
m := gomail.NewMessage()
|
||||||
|
m.SetHeader("From", viper.GetString("email_sender"))
|
||||||
|
m.SetHeader("To", email)
|
||||||
|
m.SetHeader("Subject", "Minetest 2-factor confirmation request")
|
||||||
|
m.SetBody("text/plain", message)
|
||||||
|
|
||||||
|
d := gomail.Dialer{Host: viper.GetString("smtp_server"), Port: viper.GetInt("smtp_port")}
|
||||||
|
if viper.GetBool("smtp_verify_certificate") == false {
|
||||||
|
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
}
|
||||||
|
if err := d.DialAndSend(m); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type FastCGIServer struct{}
|
||||||
|
|
||||||
|
func (s FastCGIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// track IP of client, we'll need it later for some transactions
|
||||||
|
ip, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
log.Print("Request: unable to identify peer\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteip := net.ParseIP(ip).String()
|
||||||
|
|
||||||
|
// parse POST data
|
||||||
|
body, err := ioutil.ReadAll(req.Body)
|
||||||
|
defer req.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
log.Printf("Request: %v: unable to read body\n", remoteip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var rq tfa_request
|
||||||
|
err = json.Unmarshal(body, &rq)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
log.Printf("Request: %v: malformed JSON data\n", remoteip)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rq.Remote_ip = remoteip
|
||||||
|
|
||||||
|
// create response
|
||||||
|
var rp tfa_response
|
||||||
|
|
||||||
|
// prefetch server name for requestst that want it
|
||||||
|
servername := "(no name)"
|
||||||
|
if rq.Request_type == "REG" || rq.Request_type == "AUTH" {
|
||||||
|
// get relevant info, ignore errors
|
||||||
|
var data []byte
|
||||||
|
err = db.QueryRow("SELECT data FROM servers WHERE server_id=?", rq.Server_id).Scan(&data)
|
||||||
|
if err == nil {
|
||||||
|
var sd Serverdata
|
||||||
|
err = json.Unmarshal(data, &sd)
|
||||||
|
if err == nil && sd.Name != "" {
|
||||||
|
servername = sd.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate request origin is valid
|
||||||
|
var orip string
|
||||||
|
err = db.QueryRow("SELECT ip FROM servers WHERE server_id=?", rq.Server_id).Scan(&orip)
|
||||||
|
if err != nil || orip != remoteip {
|
||||||
|
switch rq.Request_type {
|
||||||
|
case "CONFIRM":
|
||||||
|
case "SERVER":
|
||||||
|
case "SERVERSTAT":
|
||||||
|
case "SERVERIP":
|
||||||
|
case "SERVERIPSTAT":
|
||||||
|
default:
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "The server IP address changed. The server owner will need to confirm\nthis change before normal events can be handled again.", nil}
|
||||||
|
goto send_response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process request
|
||||||
|
switch rq.Request_type {
|
||||||
|
case "REG":
|
||||||
|
if rq.Email == "" || rq.Player == "" || rq.Server_id == "" {
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed, insufficient data.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !validate_email(rq.Email) {
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed, email invalid.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// check identities table for rq.Email
|
||||||
|
var created int64
|
||||||
|
err := db.QueryRow("SELECT created FROM identities WHERE email=?", rq.Email).Scan(&created)
|
||||||
|
if err == nil {
|
||||||
|
// already created? could just happen
|
||||||
|
var created int64
|
||||||
|
err = db.QueryRow("SELECT created FROM players WHERE email=? AND player=?", rq.Email, rq.Player).Scan(&created)
|
||||||
|
if err == nil {
|
||||||
|
rp = tfa_response{"REGOK", "This email is already registered.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// store the server/player combo
|
||||||
|
_, err = db.Exec("INSERT INTO players(email, name, server_id, created, data) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
rq.Email, rq.Player, rq.Server_id, time.Now().Unix(), "{}")
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"REGFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// looks like this was already registered as an identity!
|
||||||
|
rp = tfa_response{"REGOK", "This email is already registered.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// no existing identity
|
||||||
|
token := make_token()
|
||||||
|
cookie := make_token()
|
||||||
|
|
||||||
|
// send the confirmation email
|
||||||
|
if !do_email(rq.Email,
|
||||||
|
"\nHello,\n\n"+
|
||||||
|
"You've received this request because you or someone registered this email\n"+
|
||||||
|
"address on a minetest server at \""+servername+"\".\n\n"+
|
||||||
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
||||||
|
"click the following link to confirm your registration:\n\n"+
|
||||||
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed, unable to send email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the token
|
||||||
|
j, err := json.Marshal(rq)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
||||||
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
t.token, t.cookie, t.created, t.expiry, t.request)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"REGPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Cookie"] = cookie
|
||||||
|
case "REGSTAT":
|
||||||
|
var created int64
|
||||||
|
err := db.QueryRow("SELECT created FROM identities WHERE email=?", rq.Email).Scan(&created)
|
||||||
|
if err == nil {
|
||||||
|
rp = tfa_response{"REGOK", "Registration succeeded.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// check if a token exists.
|
||||||
|
var confirmed bool
|
||||||
|
err = db.QueryRow("SELECT confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(&confirmed)
|
||||||
|
if err == nil {
|
||||||
|
if confirmed {
|
||||||
|
// badness
|
||||||
|
rp = tfa_response{"REGFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rp = tfa_response{"REGPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rp = tfa_response{"REGFAIL", "Registration failed.", nil}
|
||||||
|
case "AUTH":
|
||||||
|
// check server token + username combo exists
|
||||||
|
var email string
|
||||||
|
err := db.QueryRow("SELECT email FROM players WHERE name=? AND server_id=?", rq.Player, rq.Server_id).Scan(&email)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Authentication failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// send an AUTH email
|
||||||
|
token := make_token()
|
||||||
|
cookie := make_token()
|
||||||
|
|
||||||
|
// send the confirmation email
|
||||||
|
if !do_email(email,
|
||||||
|
"\nHello,\n\n"+
|
||||||
|
"You've received this request because you or someone wants to authenticate\n"+
|
||||||
|
"using this email address on a minetest server at \""+servername+"\".\n\n"+
|
||||||
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
||||||
|
"click the following link to confirm your authentication:\n\n"+
|
||||||
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, unable to send email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the token
|
||||||
|
j, err := json.Marshal(rq)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
||||||
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
t.token, t.cookie, t.created, t.expiry, t.request)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Authentication failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"AUTHPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Cookie"] = cookie
|
||||||
|
case "AUTHSTAT":
|
||||||
|
// check tokens
|
||||||
|
var confirmed bool
|
||||||
|
var expiry int64
|
||||||
|
err := db.QueryRow("SELECT confirmed, expiry FROM tokens WHERE cookie=?", rq.Cookie).Scan(&confirmed, &expiry)
|
||||||
|
if err != nil {
|
||||||
|
// there is no token
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Server registration failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().Unix() > expiry {
|
||||||
|
rp = tfa_response{"AUTHFAIL", "Authentication failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
rp = tfa_response{"AUTHPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
rp = tfa_response{"AUTHOK", "Authentication succeeded.", nil}
|
||||||
|
case "ACCT":
|
||||||
|
// this is sent by a server to see if the account is required to
|
||||||
|
// authenticate, and / or to inspect stat data (not implemented yet)
|
||||||
|
var data []byte
|
||||||
|
var email string
|
||||||
|
err := db.QueryRow("SELECT data, email FROM players WHERE name=? AND server_id=?", rq.Player, rq.Server_id).Scan(&data, &email)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"ACCTFAIL", "Request failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var pd Player_data
|
||||||
|
err = json.Unmarshal(data, &pd)
|
||||||
|
if pd.Auth_required != "1" {
|
||||||
|
err := db.QueryRow("SELECT data FROM identities WHERE email=?", email).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"ACCTFAIL", "Request failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &pd)
|
||||||
|
if pd.Auth_required != "1" {
|
||||||
|
rp = tfa_response{"ACCTOK", "Account info retrieved.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rp = tfa_response{"ACCTOK", "Account info retrieved. Player must authenticate", nil}
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Auth_required"] = "1"
|
||||||
|
// get playerdata struct json
|
||||||
|
case "UPDATES":
|
||||||
|
// check if server sent server_data changes
|
||||||
|
if rq.Serverdata.Owner != "" {
|
||||||
|
// refresh server data
|
||||||
|
//FIXME make sure all the required fields are present again
|
||||||
|
s, err := json.Marshal(rq.Serverdata)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error storing Serverdata")
|
||||||
|
} else {
|
||||||
|
_, err = db.Exec("UPDATE servers set data=? WHERE server_id=?",
|
||||||
|
s, rq.Server_id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error updating Serverdata")
|
||||||
|
} else {
|
||||||
|
log.Println(remoteip + ": Updated server info for \"" + servername + "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//FIXME implement some updates - rp = tfa_response{"UPDATE", "Changes requested.", nil}
|
||||||
|
rp = tfa_response{"NOUPDATES", "No changes for server.", nil}
|
||||||
|
case "SERVER":
|
||||||
|
// server registration request.
|
||||||
|
if rq.Email == "" {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Registration failed, insufficient data.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if rq.Server_id != "" {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Registration failed, you provided a server id. This server is already registered.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !validate_email(rq.Email) {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Registration failed, email invalid.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate serverdata is sufficient
|
||||||
|
if rq.Serverdata.Owner == "" {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Registration failed, insufficient data.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
token := make_token()
|
||||||
|
cookie := make_token()
|
||||||
|
|
||||||
|
// send the confirmation email
|
||||||
|
if !do_email(rq.Email,
|
||||||
|
"\nHello,\n\n"+
|
||||||
|
"You've received this request because you or someone wants to register\n"+
|
||||||
|
"a server using this email address at \""+remoteip+"\".\n\n"+
|
||||||
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
||||||
|
"click the following link to confirm your server registration:\n\n"+
|
||||||
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, unable to send email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the token as "server_id" in the request
|
||||||
|
rq.Server_id = token
|
||||||
|
|
||||||
|
// store the token including original request data
|
||||||
|
j, err := json.Marshal(rq)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
||||||
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
token, cookie, t.created, t.expiry, t.request)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Server registration failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"SERVERPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
// send the token to the server, so that the server can use it to validate the registration
|
||||||
|
// after confirmation
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Cookie"] = cookie
|
||||||
|
case "SERVERIP":
|
||||||
|
// server IP address request.
|
||||||
|
if rq.Email == "" || rq.Server_id == "" {
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, insufficient data.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate email is actually the one on file for this server
|
||||||
|
var serverid, email string
|
||||||
|
err := db.QueryRow("SELECT server_id, email FROM servers WHERE server_id=? and email=?", rq.Server_id, rq.Email).Scan(&serverid, &email)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, invalid data.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
token := make_token()
|
||||||
|
cookie := make_token()
|
||||||
|
|
||||||
|
// send the confirmation email
|
||||||
|
if !do_email(rq.Email,
|
||||||
|
"\nHello,\n\n"+
|
||||||
|
"You've received this request because you or someone wants to change the IP\n"+
|
||||||
|
"address of a server using this email address at \""+remoteip+"\".\n\n"+
|
||||||
|
"If this wasn't you, you can safely ignore this email. If it was you, please\n"+
|
||||||
|
"click the following link to confirm your server IP change:\n\n"+
|
||||||
|
" " + viper.GetString("base_url") + "confirm?t="+token+"\n\n") {
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, unable to send email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the token including original request data
|
||||||
|
j, err := json.Marshal(rq)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t := Token{token, cookie, time.Now().Unix(), time.Now().Unix() + 300, j, false}
|
||||||
|
_, err = db.Exec("INSERT INTO tokens(token, cookie, created, expiry, request) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
token, cookie, t.created, t.expiry, t.request)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed, internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"SERVERIPPEND", "Mail sent. Check your mailbox.", nil}
|
||||||
|
// send the token to the server, so that the server can use it to validate the registration
|
||||||
|
// after confirmation
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Cookie"] = cookie
|
||||||
|
case "SERVERSTAT":
|
||||||
|
// verify that server_id is registered
|
||||||
|
var token string
|
||||||
|
var expiry int64
|
||||||
|
var confirmed bool
|
||||||
|
err := db.QueryRow("SELECT token, expiry, confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(
|
||||||
|
&token, &expiry, &confirmed)
|
||||||
|
if err == nil {
|
||||||
|
var created int
|
||||||
|
err := db.QueryRow("SELECT created FROM servers WHERE server_id=?", token).Scan(&created)
|
||||||
|
if err == nil {
|
||||||
|
rp = tfa_response{"SERVEROK", "Server registration succeeded.", nil}
|
||||||
|
rp.Data = make(map[string]string)
|
||||||
|
rp.Data["Server_id"] = token
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Unix() > expiry {
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Server registration failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
rp = tfa_response{"SERVERPEND", "Server registration pending. Check your email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"SERVERFAIL", "Server registration failed. Try again", nil}
|
||||||
|
break
|
||||||
|
case "SERVERIPSTAT":
|
||||||
|
// verify that server_id is registered
|
||||||
|
var expiry int64
|
||||||
|
var confirmed bool
|
||||||
|
err := db.QueryRow("SELECT expiry, confirmed FROM tokens WHERE cookie=?", rq.Cookie).Scan(
|
||||||
|
&expiry, &confirmed)
|
||||||
|
if err == nil {
|
||||||
|
var ip string
|
||||||
|
err := db.QueryRow("SELECT ip FROM servers WHERE server_id=?", rq.Server_id).Scan(&ip)
|
||||||
|
if err == nil {
|
||||||
|
if rq.Remote_ip == ip {
|
||||||
|
rp = tfa_response{"SERVERIPOK", "Server IP change succeeded.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Unix() > expiry {
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !confirmed {
|
||||||
|
rp = tfa_response{"SERVERIPPEND", "Server IP change pending. Check your email.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rp = tfa_response{"SERVERIPFAIL", "Server IP change failed. Try again", nil}
|
||||||
|
break
|
||||||
|
|
||||||
|
//
|
||||||
|
// www initiated requests
|
||||||
|
//
|
||||||
|
case "CONFIRM":
|
||||||
|
if rq.Token == "" {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Invalid confirmation.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch entry from tokens table
|
||||||
|
var ot Token
|
||||||
|
err := db.QueryRow("SELECT created, expiry, request, confirmed FROM tokens WHERE token=?",
|
||||||
|
rq.Token).Scan(&ot.created, &ot.expiry, &ot.request, &ot.confirmed)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we didn't already do this
|
||||||
|
if ot.confirmed {
|
||||||
|
rp = tfa_response{"CONFIRMOK", "Request already completed before.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if token not too old
|
||||||
|
if time.Now().Unix() > ot.expiry {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Request token expired. Create a new request.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch original request
|
||||||
|
var or tfa_request
|
||||||
|
err = json.Unmarshal(ot.request, &or)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete the transaction!
|
||||||
|
if or.Request_type == "REG" {
|
||||||
|
_, err = db.Exec("INSERT INTO identities (email, created, data) VALUES (?, ?, ?)",
|
||||||
|
or.Email, time.Now().Unix(), "{}")
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as done
|
||||||
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
||||||
|
true, rq.Token)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the server/player combo
|
||||||
|
_, err = db.Exec("INSERT INTO players(email, name, server_id, created, data) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
or.Email, or.Player, or.Server_id, time.Now().Unix(), "{}")
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing left to do
|
||||||
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your identity is now registered.", nil}
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if or.Request_type == "AUTH" {
|
||||||
|
// Mark as done
|
||||||
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
||||||
|
true, rq.Token)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing left to do
|
||||||
|
rp = tfa_response{"CONFIRMOK", "Request completed. You are now authenticated.", nil}
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if or.Request_type == "SERVER" {
|
||||||
|
s, err := json.Marshal(or.Serverdata)
|
||||||
|
if err != nil {
|
||||||
|
s = []byte("{}")
|
||||||
|
log.Println("Error storing Serverdata")
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO servers (server_id, email, created, data, ip) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
or.Server_id, or.Email, time.Now().Unix(), s, or.Remote_ip)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as done
|
||||||
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
||||||
|
true, rq.Token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing left to do
|
||||||
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your server is now registered.", nil}
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if or.Request_type == "SERVERIP" {
|
||||||
|
_, err = db.Exec("UPDATE servers SET ip=? WHERE server_id=?",
|
||||||
|
or.Remote_ip, or.Server_id)
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as done
|
||||||
|
_, err = db.Exec("UPDATE tokens SET confirmed=? WHERE token=?",
|
||||||
|
true, rq.Token)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing left to do
|
||||||
|
rp = tfa_response{"CONFIRMOK", "Request completed. Your server IP address now changed.", nil}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
rp = tfa_response{"CONFIRMFAIL", "Internal server error.", nil}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
//passwdchange
|
||||||
|
//emailchange
|
||||||
|
default:
|
||||||
|
rp = tfa_response{"UNK", "Unknown request type. Don't do that again.", nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
send_response:
|
||||||
|
// and send to the client
|
||||||
|
output, err := json.Marshal(rp)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
log.Print("Response: formatting response failed\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%v: %v->%v (%v) \"%v\"\n",
|
||||||
|
remoteip, rq.Request_type, rp.Result,
|
||||||
|
rq.Player, rp.Info)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(output)
|
||||||
|
|
||||||
|
//FIXME prune tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
// config stuffs
|
||||||
|
viper.SetConfigName("mt2fa")
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
|
viper.AddConfigPath("/usr/share/defaults/etc")
|
||||||
|
viper.AddConfigPath("/etc")
|
||||||
|
viper.AddConfigPath("$HOME/.config")
|
||||||
|
|
||||||
|
viper.SetDefault("socket", "/run/mt2fa/sock")
|
||||||
|
|
||||||
|
viper.SetDefault("email_sender", "nobody@localhost.localdomain")
|
||||||
|
viper.SetDefault("smtp_server", "localhost")
|
||||||
|
viper.SetDefault("smtp_port", 587)
|
||||||
|
viper.SetDefault("smtp_verify_certificate", true)
|
||||||
|
viper.SetDefault("sqlite_db", "mt2fa.sqlite")
|
||||||
|
viper.SetDefault("base_url", "https://localhost/")
|
||||||
|
|
||||||
|
err := viper.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error in confog file: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen on fcgi socket
|
||||||
|
s := viper.GetString("socket")
|
||||||
|
os.Remove(s)
|
||||||
|
|
||||||
|
listener, err := net.Listen("unix", s)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("mt2fa: net.Listen: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Chmod(s, 0666)
|
||||||
|
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// open our db
|
||||||
|
db, err = sql.Open("sqlite3", viper.GetString("sqlite_db"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// initialize our db as needed
|
||||||
|
createStmt := `
|
||||||
|
CREATE TABLE IF NOT EXISTS tokens (
|
||||||
|
token TEXT NOT NULL PRIMARY KEY,
|
||||||
|
cookie TEXT NOT NULL,
|
||||||
|
created INTEGER NOT NULL,
|
||||||
|
expiry INTEGER NOT NULL,
|
||||||
|
request TEXT NOT NULL,
|
||||||
|
confirmed BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS servers (
|
||||||
|
server_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created INTEGER NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS identities (
|
||||||
|
email TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
server_id TEXT NOT NULL,
|
||||||
|
created INTEGER NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
);
|
||||||
|
`
|
||||||
|
_, err = db.Exec(createStmt)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("%q: %s\n", err, createStmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve requests.
|
||||||
|
h := new(FastCGIServer)
|
||||||
|
|
||||||
|
log.Print("mt2fa: started")
|
||||||
|
|
||||||
|
err = fcgi.Serve(listener, h)
|
||||||
|
}
|
290
readme.md
Normal file
290
readme.md
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
|
||||||
|
## mt2fa-server - Minetest 2factor auth
|
||||||
|
|
||||||
|
Provides a simple 2-factor auth module for verifying and maintaining
|
||||||
|
verification of player identities. Players are required to register
|
||||||
|
an email address with the 2fa service, and they will receive a login
|
||||||
|
token through the 2fa service, which is needed for all logins.
|
||||||
|
|
||||||
|
A remote 2fa service handles the sending and receiving of emails and
|
||||||
|
creating the 2fa tokens. This server can be used by many different
|
||||||
|
servers at the same time.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
(C) 2018 Auke Kok <sofar@foo-projects.org>
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software
|
||||||
|
for any purpose with or without fee is hereby granted, provided
|
||||||
|
that the above copyright notice and this permission notice appear
|
||||||
|
in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
|
||||||
|
BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
|
||||||
|
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
## Operation modes
|
||||||
|
|
||||||
|
### 2FA registration voluntary / required
|
||||||
|
|
||||||
|
In this simplified mode, an account may, or must be linked to an
|
||||||
|
email address, but login does not require a 2FA token. This allows
|
||||||
|
players to recover their lost passwords without requiring the player
|
||||||
|
to provide a valid 2FA token on each login.
|
||||||
|
|
||||||
|
### 2FA authentication voluntary / required
|
||||||
|
|
||||||
|
In this mode, the player may, or must enroll in full 2FA authentication
|
||||||
|
on a voluntary basis. The player must provide a valid 2FA token on
|
||||||
|
each login after enrollment.
|
||||||
|
|
||||||
|
If authentication is required, but registration isn't, then players
|
||||||
|
who registered are required to authenticate with 2fa, but others can
|
||||||
|
continue to use normal login.
|
||||||
|
|
||||||
|
Each server operator can set minimum requirements. E.g. a server could
|
||||||
|
require registration, but may leave authentication voluntary.
|
||||||
|
|
||||||
|
## client mod
|
||||||
|
|
||||||
|
A client mod handles login of new players and requires them to provide
|
||||||
|
a valid email address. The 2fa service then is queried and if the email
|
||||||
|
verification succeeds, the client will receive an email with a valid
|
||||||
|
2fa login token. Once the login token is entered into the game UI, or
|
||||||
|
confirmed back to the 2FA service, the player is granted permissions
|
||||||
|
to interact with the game.
|
||||||
|
|
||||||
|
## 2fa server
|
||||||
|
|
||||||
|
The 2fa server handles incoming game server requests for either new
|
||||||
|
registrations, or for login events. These result in emails being
|
||||||
|
sent to the email address. In the registration email, the user is
|
||||||
|
simply requested to `reply` to the email. In the login event, the
|
||||||
|
user is provided a one-time password. The user has to enter this
|
||||||
|
one time password into their game client in order to access game
|
||||||
|
functionality. Or, the user clicks a link in the email, or, replies
|
||||||
|
to the email to confirm.
|
||||||
|
|
||||||
|
The 2FA server then provides a limited time token to the authentication
|
||||||
|
mod on the game server.
|
||||||
|
|
||||||
|
## account registration
|
||||||
|
|
||||||
|
Accounts are registered with the 2fa server, which holds the database
|
||||||
|
with all the valid accounts. These accounts can be confirmed in
|
||||||
|
several ways. The main key for each account is a valid e-mail address.
|
||||||
|
|
||||||
|
The player receives account emails on their registered email address.
|
||||||
|
In the registration confirmation, a token is embedded that can be used
|
||||||
|
to activate the account and therefore make it playable.
|
||||||
|
|
||||||
|
The user can confirm the account by responding back to the 2fa server.
|
||||||
|
This can be done in several ways.
|
||||||
|
|
||||||
|
1. https - the user clicks a link with a token in the email.
|
||||||
|
|
||||||
|
Not implemented:
|
||||||
|
|
||||||
|
2. smtp - the user replies to the email with the token.
|
||||||
|
3. in game - the user enters the token into the game.
|
||||||
|
|
||||||
|
All these methods result in a confirmation event being sent to the 2fa
|
||||||
|
server, and then activation of the account in the account database.
|
||||||
|
|
||||||
|
## account login
|
||||||
|
|
||||||
|
If required by the game server, authentication is performed by sending the
|
||||||
|
player an email with a token that the player needs to reply, click, or
|
||||||
|
enter in the game client. Once the game server receives the token through
|
||||||
|
either direct input, or by confirmation from the 2FA server, the client is
|
||||||
|
granted interact privileges on the game server.
|
||||||
|
|
||||||
|
## account recovery
|
||||||
|
|
||||||
|
If the player forgets their game server password, the client can attempt
|
||||||
|
recovery of the game server account by initiating an account recovery.
|
||||||
|
The game server passes the account recovery request to the 2FA server,
|
||||||
|
which requests confirmation by the player through e-mail. Once the player
|
||||||
|
confirms the token through email, click or direct input, the client
|
||||||
|
may enter a new password for the game server directly into the game
|
||||||
|
server.
|
||||||
|
|
||||||
|
## account exclusivity
|
||||||
|
|
||||||
|
(not implemented)
|
||||||
|
|
||||||
|
If the player opts in, or, if the server requires it, the account
|
||||||
|
becomes single-use. This will force the player offline on a server
|
||||||
|
if the player logs in on another server correctly.
|
||||||
|
|
||||||
|
## account monitoring
|
||||||
|
|
||||||
|
(not implemented)
|
||||||
|
|
||||||
|
Players can monitor and audit their own account information,
|
||||||
|
authentication requests, failures, recovery attempts and other data
|
||||||
|
that resides on the 2FA server.
|
||||||
|
|
||||||
|
## protocols
|
||||||
|
|
||||||
|
The server communicates in several ways through other protocols:
|
||||||
|
|
||||||
|
SMTP:
|
||||||
|
- only outgoing. The server sends emails over SMTP.
|
||||||
|
- if a user replies, a smtp to https bridge will assure the server
|
||||||
|
sees the verification
|
||||||
|
|
||||||
|
HTTPS:
|
||||||
|
- incoming:
|
||||||
|
- To receive new account requests
|
||||||
|
- to receive 2fa login requests
|
||||||
|
- over the web: receive account verification link clicks
|
||||||
|
- bidir:
|
||||||
|
- answer 2fa login status requests
|
||||||
|
|
||||||
|
## banning/abuse
|
||||||
|
|
||||||
|
(not implemented)
|
||||||
|
|
||||||
|
The 2fa server can store account status information, and the game server
|
||||||
|
can retrieve this information.
|
||||||
|
|
||||||
|
The following data items are stored for each account:
|
||||||
|
|
||||||
|
ro: data can be retrieved for the account
|
||||||
|
rw: data can be provided/changed for the account
|
||||||
|
public: all servers may access this data
|
||||||
|
private: the data is private to the server
|
||||||
|
|
||||||
|
- creation date (public, ro)
|
||||||
|
- banned here (private, rw)
|
||||||
|
- bancount here (private, ro)
|
||||||
|
- playername (private, rw)
|
||||||
|
- banned (public, ro)
|
||||||
|
- bancount (public, ro)
|
||||||
|
|
||||||
|
The 2fa server does not *act* on this information, it merely provides
|
||||||
|
it to the servers and acts as a storage of this information. Any player
|
||||||
|
objection on how this data is *used* by the server owner should be
|
||||||
|
directed to the server owner.
|
||||||
|
|
||||||
|
## changing email addresses
|
||||||
|
|
||||||
|
(not implemented)
|
||||||
|
|
||||||
|
requires access to both old and new accounts, and verification from both
|
||||||
|
those accounts, by SMTP mails to both addresses.
|
||||||
|
|
||||||
|
## scenario's
|
||||||
|
|
||||||
|
* registration
|
||||||
|
|
||||||
|
gameserver -> "REG" packet -> m2tfa
|
||||||
|
mt2fa -> "REGPEND" -> gameserver
|
||||||
|
mt2fa -> "REGFAIL" -> gameserver
|
||||||
|
mt2fa -> SMTP cookie -> mailbox
|
||||||
|
mailbox -> click link -> mt2fa (reg ok/reg fail)
|
||||||
|
|
||||||
|
gameserver -> "REGSTAT" -> mt2fa
|
||||||
|
mt2fa -> REGOK
|
||||||
|
mt2fa -> REGPEND
|
||||||
|
mt2fa -> REGFAIL
|
||||||
|
|
||||||
|
* authentication
|
||||||
|
|
||||||
|
gameserver -> "AUTH" -> mt2fa
|
||||||
|
mt2fa -> "AUTHPEND" -> gameserver
|
||||||
|
mt2fa -> "AUTHFAIL" -> gameserver
|
||||||
|
mt2fa -> SMTP cookie -> mailbox
|
||||||
|
mailbox -> click link -> mt2fa (auth ok/auth fail)
|
||||||
|
|
||||||
|
gameserver -> "AUTHSTAT" -> mt2fa
|
||||||
|
mt2fa -> AUTHOK
|
||||||
|
mt2fa -> AUTHFAIL
|
||||||
|
mt2fa -> AUTHPEND
|
||||||
|
|
||||||
|
* fetching account info at login (optional auth)
|
||||||
|
|
||||||
|
gameserver -> "ACCT" -> mt2fa
|
||||||
|
mt2fa -> ACCTOK -> gameserver
|
||||||
|
mt2fa -> ACCTFAIL -> gameserver
|
||||||
|
|
||||||
|
* passreset request
|
||||||
|
|
||||||
|
javascript form -> post -> mt2fa
|
||||||
|
mt2fa -> SMTP -> mailbox !confirmation
|
||||||
|
mailbox -> click link -> mt2fa
|
||||||
|
mt2fa -> SMTP -> mailbox !new password
|
||||||
|
|
||||||
|
gameserver -> "UPDATES" -> mt2fa
|
||||||
|
mt2fa -> "UPDATE" -> gameserver
|
||||||
|
mt2fa -> "NOUPDATES" -> gameserver
|
||||||
|
|
||||||
|
* email change
|
||||||
|
|
||||||
|
javascript form -> post -> mt2fa
|
||||||
|
mt2fa -> SMTP -> mailbox !confirmation_old_mailbox
|
||||||
|
mailbox -> click link -> mt2fa
|
||||||
|
mt2fa -> SMTP -> mailbox !confirmation_new_mailbox
|
||||||
|
mailbox -> click link -> mt2fa
|
||||||
|
|
||||||
|
gameserver -> "UPDATES" -> mt2fa
|
||||||
|
mt2fa -> "UPDATE" -> gameserver
|
||||||
|
mt2fa -> "NOUPDATES" -> gameserver
|
||||||
|
|
||||||
|
* server enrollment
|
||||||
|
|
||||||
|
gameserver -> SERVER -> mt2fa
|
||||||
|
mt2fa -> SERVERFAIL -> gameserver
|
||||||
|
mt2fa -> SERVERPEND -> gameserver
|
||||||
|
mt2fa -> SMTP -> mailbox !confirmation
|
||||||
|
mailbox -> click link -> mt2fa
|
||||||
|
|
||||||
|
gameserver -> SERVERSTAT -> mt2fa
|
||||||
|
mt2fa -> SERVERFAIL -> gameserver
|
||||||
|
mt2fa -> SERVERPEND -> gameserver
|
||||||
|
mt2fa -> SERVEROK -> gameserver
|
||||||
|
|
||||||
|
-> SERVERIP ->
|
||||||
|
SERVERIPFAIL
|
||||||
|
SERVERIPPEND
|
||||||
|
SERVERIPOK
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
CREATE TABLE tokens (
|
||||||
|
token TEXT NOT NULL PRIMARY KEY, -- secret
|
||||||
|
cookie TEXT NOT NULL, -- non-secret
|
||||||
|
created INTEGER NOT NULL, -- DATE first created
|
||||||
|
expiry INTEGER NOT NULL, -- timestamp when no longer valid
|
||||||
|
request TEXT NOT NULL, -- the JSON request context
|
||||||
|
confirmed BOOLEAN DEFAULT FALSE, -- whether a token has been confirmed by a user
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE servers (
|
||||||
|
server_id TEXT NOT NULL PRIMARY KEY, -- unique identifier, arbitrary string
|
||||||
|
created INTEGER NOT NULL, -- DATE first created
|
||||||
|
email TEXT NOT NULL, -- associated admin, for confirmation requests
|
||||||
|
data TEXT NOT NULL, -- JSON encoded data, expandable data format
|
||||||
|
ip TEXT NOT NULL, -- used to prevent token stealing/spoofing
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE identities (
|
||||||
|
email TEXT NOT NULL PRIMARY KEY, -- unique identifier, must be valid email
|
||||||
|
created INTEGER NOT NULL, -- DATE first created
|
||||||
|
data TEXT NOT NULL, -- JSON encoded data, expandable data format
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE players (
|
||||||
|
email TEXT NOT NULL, -- identity
|
||||||
|
name TEXT NOT NULL, -- local player name
|
||||||
|
server_id TEXT NOT NULL, -- unique server identity
|
||||||
|
created INTEGER NOT NULL, -- DATE created for this server
|
||||||
|
data TEXT NOT NULL, -- JSON encoded data, expandable data format
|
||||||
|
)
|
||||||
|
|
2
www/beauty.min.css
vendored
Normal file
2
www/beauty.min.css
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*{margin:0;padding:0;box-sizing:border-box}body{font-family:"Segoe UI", "San Francisco", sans-serif;background-color:#f8f8f8;color:#333;line-height:1.6;max-width:95%;margin:0 auto}::-moz-selection{background-color:#2297e0;color:#fff}::selection{background-color:#2297e0;color:#fff}@media (min-width: 40rem){body{max-width:90%}}@media (min-width: 60rem){body{max-width:80%}}@media (min-width: 80rem){body{max-width:70%}}@media (min-width: 100rem){body{max-width:60%}}h1{margin:2rem 0;text-align:center}h2{margin:1.5rem 0}h3{margin:1rem 0}h4{margin:0.75rem 0}h5{margin:0.75rem 0}h6{margin:0.75rem 0}p{margin:1rem 0}li{list-style-position:inside}code,pre{background-color:#e4e4e4;color:#333;padding:0.125rem;border-radius:0.25rem;font-size:0.875rem}pre{white-space:pre-wrap;tab-size:2}kbd{background-color:#fff;color:#333;border:1px solid #ccc;padding:0.125rem;border-radius:0.25rem}blockquote{border-left:3px solid #999;padding:0.75rem 0 0.75rem 1rem;color:#777;background-color:#eee}footer{text-align:center;color:#888;padding:0.375rem;margin-top:3rem}a{color:#2297e0;text-decoration:none;transition:0.15s background-color}a:hover{background-color:#bfe1f6}a:active{background-color:#92ccf0}table,th,td{text-align:left;padding:0.2em;margin:0px;border:1px solid;border-collapse:collapse;}
|
||||||
|
.modal{display:none;position:fixed;z-index:1;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4);}.modal-content{background-color:#fefefe;margin:15% auto;padding:20px;border:1px solid #888;width:95%;}.close{color:#aaa;float:right;font-size:28px;font-weight:bold;}.close:hover,.close:focus{color:black;text-decoration:none;cursor:pointer;}
|
11
www/confirm
Normal file
11
www/confirm
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>MT2FA 2-Factor Authentication - Confirmation</title>
|
||||||
|
<link rel="stylesheet" href="beauty.min.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Your request results:</h3>
|
||||||
|
<pre><div id="c">Javascript is required for this webpage to function.</div></pre>
|
||||||
|
<script src="/confirm.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
39
www/confirm.js
Normal file
39
www/confirm.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
var urlParams;
|
||||||
|
(window.onpopstate = function () {
|
||||||
|
var match,
|
||||||
|
pl = /\+/g, // Regex for replacing addition symbol with a space
|
||||||
|
search = /([^&=]+)=?([^&]*)/g,
|
||||||
|
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
|
||||||
|
query = window.location.search.substring(1);
|
||||||
|
|
||||||
|
urlParams = {};
|
||||||
|
while (match = search.exec(query))
|
||||||
|
urlParams[decode(match[1])] = decode(match[2]);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Let the user know JS is working by removing the "JS is needed" text
|
||||||
|
document.getElementById("c").innerHTML = "Submitting confirmation data...";
|
||||||
|
|
||||||
|
// Create JSON data to post
|
||||||
|
var data = {};
|
||||||
|
data["token"] = urlParams["t"];
|
||||||
|
data["request_type"] = "CONFIRM";
|
||||||
|
|
||||||
|
// POST it
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "/mt2fa", true);
|
||||||
|
xhr.setRequestHeader("Content-Type", 'application/json');
|
||||||
|
xhr.send(JSON.stringify(data))
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) {
|
||||||
|
// Parse reply and display msg to user
|
||||||
|
var data = JSON.parse(xhr.responseText);
|
||||||
|
if (data.result == "CONFIRMOK") {
|
||||||
|
document.getElementById("c").innerHTML = "OK: " + data.info;
|
||||||
|
} else {
|
||||||
|
document.getElementById("c").innerHTML = "ERROR: " + data.result + " - " + data.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user