fulgens/services-src/auth/main.go

1788 lines
59 KiB
Go
Raw Normal View History

2024-09-28 19:41:34 +01:00
package main
import (
// Fulgens libraries
library "git.ailur.dev/ailur/fg-library/v2"
authLibrary "git.ailur.dev/ailur/fg-nucleus-library"
"git.ailur.dev/ailur/pow"
2024-09-28 19:41:34 +01:00
// Standard libraries
"bytes"
"errors"
"strconv"
"strings"
"time"
// Secondary libraries
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/binary"
"encoding/json"
"html/template"
"io/fs"
"net/http"
// Extra libraries
"golang.org/x/crypto/argon2"
// External libraries
"github.com/cespare/xxhash/v2"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
2024-09-28 19:41:34 +01:00
"github.com/google/uuid"
_ "modernc.org/sqlite"
)
var ServiceInformation = library.Service{
Name: "Authentication",
Permissions: library.Permissions{
Authenticate: false, // This service *is* the authentication service
Database: true, // This service does require database access
BlobStorage: false, // This service does not require blob storage
InterServiceCommunication: true, // This service does require inter-service communication
Resources: true, // This service does require its HTTP templates and static files to be served
},
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"),
}
var serviceIDBytes []byte
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
// Log the message to the logger service
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
2024-09-28 19:41:34 +01:00
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), // Logger service
MessageType: messageType,
SentAt: time.Now(),
Message: message,
}
}
func ensureTrailingSlash(url string) string {
if !strings.HasSuffix(url, "/") {
return url + "/"
}
return url
}
func randomChars(length int) (string, error) {
var saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if length <= 0 {
return "", errors.New("salt length must be greater than 0")
}
salt := make([]byte, length)
randomBytes := make([]byte, length)
_, err := rand.Read(randomBytes)
if err != nil {
return "", err
}
for i := range salt {
salt[i] = saltChars[int(randomBytes[i])%len(saltChars)]
}
return string(salt), nil
}
func sha256Base64(s string) string {
hashed := sha256.Sum256([]byte(s))
encoded := base64.URLEncoding.EncodeToString(hashed[:])
encoded = strings.TrimRight(encoded, "=")
return encoded
}
func renderTemplate(statusCode int, w http.ResponseWriter, data map[string]interface{}, templatePath string, information library.ServiceInitializationInformation) {
var err error
var requestedTemplate *template.Template
// Output ls of the resource directory
requestedTemplate, err = template.ParseFS(information.ResourceDir, "templates/"+templatePath)
if err != nil {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 01. Please report to the administrator.", information)
} else {
w.WriteHeader(statusCode)
if strings.HasSuffix(templatePath, ".html") {
w.Header().Set("Content-Type", "text/html")
} else if strings.HasSuffix(templatePath, ".json") {
w.Header().Set("Content-Type", "application/json")
} else {
w.Header().Set("Content-Type", "text/plain")
}
2024-09-28 19:41:34 +01:00
err = requestedTemplate.Execute(w, data)
if err != nil {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information)
}
}
}
func renderString(statusCode int, w http.ResponseWriter, data string, information library.ServiceInitializationInformation) {
w.Header().Set("Content-Type", "text/plain")
2024-09-28 19:41:34 +01:00
w.WriteHeader(statusCode)
_, err := w.Write([]byte(data))
if err != nil {
logFunc(err.Error(), 2, information)
}
}
func renderJSON(statusCode int, w http.ResponseWriter, data map[string]interface{}, information library.ServiceInitializationInformation) {
w.Header().Set("Content-Type", "application/json")
2024-09-28 19:41:34 +01:00
w.WriteHeader(statusCode)
err := json.NewEncoder(w).Encode(data)
if err != nil {
logFunc(err.Error(), 2, information)
}
}
func verifyJwt(token string, publicKey ed25519.PublicKey, mem *sql.DB) ([]byte, jwt.MapClaims, bool) {
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
return nil, nil, false
}
if !parsedToken.Valid {
return nil, nil, false
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
return nil, nil, false
}
// Check if the token expired
date, err := claims.GetExpirationTime()
if err != nil {
return nil, claims, false
}
if date.Before(time.Now()) {
2024-09-28 19:41:34 +01:00
return nil, claims, false
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", claims["session"]).Scan(&userId)
if err != nil {
return nil, claims, false
}
return userId, claims, true
}
func Main(information library.ServiceInitializationInformation) {
var conn library.Database
2024-09-28 19:41:34 +01:00
var mem *sql.DB
var publicKey ed25519.PublicKey
var privateKey ed25519.PrivateKey
// Load the configuration
privacyPolicy := information.Configuration["privacyPolicy"].(string)
hostName := information.Configuration["url"].(string)
testAppIsAvailable := information.Configuration["testAppEnabled"].(bool)
testAppIsInternalApp, ok := information.Configuration["testAppIsInternalApp"].(bool)
if !ok {
testAppIsAvailable = false
}
identifier := information.Configuration["identifier"].(string)
adminKey := information.Configuration["adminKey"].(string)
var err error
serviceIDBytes, err = ServiceInformation.ServiceID.MarshalBinary()
if err != nil {
logFunc(err.Error(), 3, information)
}
// Initiate a connection to the database
// Call service ID 1 to get the database connection information
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
2024-09-28 19:41:34 +01:00
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
MessageType: 1, // Request connection information
SentAt: time.Now(),
Message: nil,
}
// Wait for the response
response := <-information.Inbox
if response.MessageType == 2 {
// This is the connection information
// Set up the database connection
conn = response.Message.(library.Database)
if conn.DBType == library.Sqlite {
// Create the global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BLOB NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BLOB NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BLOB NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
// Create the global table
// Uniqueness check is a hack to ensure we only have one global row
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS global (key BYTEA NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the users table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA PRIMARY KEY NOT NULL UNIQUE, created INTEGER NOT NULL, username TEXT NOT NULL UNIQUE, publicKey BYTEA NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the oauth table
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS oauth (appId TEXT NOT NULL UNIQUE, secret TEXT, creator BYTEA NOT NULL, redirectUri TEXT NOT NULL, name TEXT NOT NULL, keyShareUri TEXT NOT NULL DEFAULT '', scopes TEXT NOT NULL DEFAULT '[\"openid\"]')")
if err != nil {
logFunc(err.Error(), 3, information)
}
}
// Set up the in-memory cache
mem, err = sql.Open("sqlite", "file:"+ServiceInformation.ServiceID.String()+"?mode=memory&cache=shared")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Drop the tables if they exist
_, err = mem.Exec("DROP TABLE IF EXISTS sessions")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS logins")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS spent")
if err != nil {
logFunc(err.Error(), 3, information)
}
_, err = mem.Exec("DROP TABLE IF EXISTS challengeResponse")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the sessions table
_, err = mem.Exec("CREATE TABLE sessions (id BLOB NOT NULL, session TEXT NOT NULL, device TEXT NOT NULL DEFAULT '?')")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the logins table
_, err = mem.Exec("CREATE TABLE logins (appId TEXT NOT NULL, exchangeCode TEXT NOT NULL UNIQUE, pkce TEXT, pkceMethod TEXT, openid BOOLEAN NOT NULL, userId BLOB NOT NULL UNIQUE, nonce TEXT NOT NULL DEFAULT '', token TEXT NOT NULL)")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the spent PoW table
_, err = mem.Exec("CREATE TABLE spent (hash BLOB NOT NULL UNIQUE, expires INTEGER NOT NULL)")
if err != nil {
logFunc(err.Error(), 3, information)
}
// Create the challenge-response table
_, err = mem.Exec("CREATE TABLE challengeResponse (challenge TEXT NOT NULL UNIQUE, userId BLOB NOT NULL, expires INTEGER NOT NULL)")
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
// This is an error message
// Log the error message to the logger service
logFunc(response.Message.(error).Error(), 3, information)
}
// Set up the signing keys
// Check if the global table has the keys
err = conn.DB.QueryRow("SELECT key FROM global LIMIT 1").Scan(&privateKey)
2024-09-28 19:41:34 +01:00
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Generate a new key
var err error
publicKey, privateKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
logFunc(err.Error(), 3, information)
}
// Insert the key into the global table
_, err = conn.DB.Exec("INSERT INTO global (key) VALUES ($1)", privateKey)
2024-09-28 19:41:34 +01:00
if err != nil {
logFunc(err.Error(), 3, information)
}
} else {
logFunc(err.Error(), 3, information)
}
} else {
publicKey = privateKey.Public().(ed25519.PublicKey)
}
// Set up the test app
_, err = conn.DB.Exec("DELETE FROM oauth WHERE appId = 'TestApp-DoNotUse'")
2024-09-28 19:41:34 +01:00
if err != nil {
testAppIsAvailable = false
logFunc(err.Error(), 2, information)
}
if testAppIsInternalApp {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", serviceIDBytes, ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
2024-09-28 19:41:34 +01:00
} else {
testAppCreator, err := uuid.New().MarshalBinary()
if err != nil {
testAppIsAvailable = false
logFunc(err.Error(), 2, information)
}
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', $1, 'Test App', $2, '[\"openid\", \"clientKeyShare\"]', $3)", testAppCreator, ensureTrailingSlash(hostName)+"testApp", ensureTrailingSlash(hostName)+"keyExchangeTester")
2024-09-28 19:41:34 +01:00
}
if err != nil {
testAppIsAvailable = false
logFunc(err.Error(), 2, information)
}
// Set up the router
router := chi.NewRouter()
2024-09-28 19:41:34 +01:00
// Set up the static routes
staticDir, err := fs.Sub(information.ResourceDir, "static")
if err != nil {
logFunc(err.Error(), 3, information)
} else {
router.Handle("/static/*", http.StripPrefix("/static/", http.FileServerFS(staticDir)))
}
router.Get("/login", func(w http.ResponseWriter, r *http.Request) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "login.html", information)
})
router.Get("/signup", func(w http.ResponseWriter, r *http.Request) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "signup.html", information)
})
router.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "logout.html", information)
})
router.Get("/privacy", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, privacyPolicy, 301)
})
router.Get("/testApp", func(w http.ResponseWriter, r *http.Request) {
if testAppIsAvailable {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "testApp.html", information)
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "testAppNotAvailable.html", information)
}
})
router.Get("/authorize", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("client_id") != "" {
if conn.DBType == library.Sqlite {
var name string
var creator []byte
err := conn.DB.QueryRow("SELECT name, creator FROM oauth WHERE appId = $1", r.URL.Query().Get("client_id")).Scan(&name, &creator)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderString(404, w, "App not found", information)
} else {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 03. Please report to the administrator.", information)
}
return
}
if !bytes.Equal(creator, serviceIDBytes) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "authorize.html", information)
2024-09-28 19:41:34 +01:00
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "autoAccept.html", information)
}
} else {
var name string
var creator uuid.UUID
err := conn.DB.QueryRow("SELECT name, creator FROM oauth WHERE appId = $1", r.URL.Query().Get("client_id")).Scan(&name, &creator)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderString(404, w, "App not found", information)
} else {
logFunc(err.Error(), 2, information)
renderString(500, w, "Sorry, something went wrong on our end. Error code: 03. Please report to the administrator.", information)
}
return
2024-09-28 19:41:34 +01:00
}
if creator != ServiceInformation.ServiceID {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "authorize.html", information)
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
"name": name,
}, "autoAccept.html", information)
}
}
2024-09-28 19:41:34 +01:00
} else {
http.Redirect(w, r, "/dashboard", 301)
2024-09-28 19:41:34 +01:00
}
})
router.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "dashboard.html", information)
})
router.Get("/clientKeyShare", func(w http.ResponseWriter, r *http.Request) {
// Parse the JWT from the query string
if r.URL.Query().Get("accessToken") == "" {
renderString(400, w, "No token provided", information)
return
}
// Verify the JWT
_, claims, ok := verifyJwt(r.URL.Query().Get("accessToken"), publicKey, mem)
if !ok {
renderString(401, w, "Invalid token", information)
return
}
// Check if they have the clientKeyShare scope
var scopes string
err = conn.DB.QueryRow("SELECT scopes FROM oauth WHERE appId = $1", claims["aud"]).Scan(&scopes)
if err != nil {
renderString(500, w, "Sorry, something went wrong on our end. Error code: 20. Please report to the administrator.", information)
logFunc(err.Error(), 2, information)
return
}
// Unmarshal the scopes
var scopesArray []string
err = json.Unmarshal([]byte(scopes), &scopesArray)
if err != nil {
renderString(500, w, "Sorry, something went wrong on our end. Error code: 21. Please report to the administrator.", information)
logFunc(err.Error(), 2, information)
return
}
// Check if the clientKeyShare scope is present
var hasClientKeyShare bool
for _, scope := range scopesArray {
if scope == "clientKeyShare" {
hasClientKeyShare = true
break
}
}
if !hasClientKeyShare {
renderString(403, w, "Missing scope", information)
return
}
// Check it's not an openid token
if claims["isOpenID"] == true {
renderString(400, w, "Invalid token", information)
} else {
renderTemplate(200, w, map[string]interface{}{
"identifier": identifier,
}, "clientKeyShare.html", information)
}
2024-09-28 19:41:34 +01:00
})
router.Get("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
renderTemplate(200, w, map[string]interface{}{
"hostName": hostName,
}, "openid.json", information)
})
router.Post("/api/changePassword", func(w http.ResponseWriter, r *http.Request) {
type changePassword struct {
Session string `json:"session"`
NewPassword string `json:"newPassword"`
}
var data changePassword
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Session).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Generate a new salt
// We want it to be binary data, not alphanumerical, so we don't use randomChars
salt := make([]byte, 16)
_, err = rand.Read(salt)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
logFunc(err.Error(), 2, information)
return
}
// Decode the new password
newPassword, err := base64.StdEncoding.DecodeString(data.NewPassword)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Hash the password
hashedPassword := argon2.IDKey(newPassword, salt, 64, 4096, 1, 32)
// Update the password
_, err = conn.DB.Exec("UPDATE users SET password = $1, salt = $2 WHERE id = $3", hashedPassword, salt, userId)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information)
logFunc(err.Error(), 2, information)
return
}
// Invalidate all sessions
_, err = mem.Exec("DELETE FROM sessions WHERE id = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "06"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return success
renderJSON(200, w, map[string]interface{}{"success": true}, information)
})
router.Post("/api/signup", func(w http.ResponseWriter, r *http.Request) {
type signup struct {
Username string `json:"username"`
PublicKey string `json:"publicKey"`
2024-09-28 19:41:34 +01:00
ProofOfWork string `json:"proofOfWork"`
}
var data signup
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the difficulty, timestamp and resource are correct
powSlice := strings.Split(data.ProofOfWork, ":")
if powSlice[0] != "2" || powSlice[3] != "fg-auth-signup" {
2024-09-28 19:41:34 +01:00
renderJSON(400, w, map[string]interface{}{"error": "Invalid PoW"}, information)
return
}
timestamp, err := strconv.ParseInt(powSlice[1], 10, 64)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid PoW"}, information)
return
}
if time.Unix(timestamp, 0).Add(time.Minute*10).Compare(time.Now()) < 0 {
renderJSON(400, w, map[string]interface{}{"error": "PoW expired"}, information)
return
}
// Verify the PoW
if !ailur_pow.VerifyPoW(data.ProofOfWork) {
2024-09-28 19:41:34 +01:00
renderJSON(400, w, map[string]interface{}{"error": "Invalid PoW"}, information)
return
}
// Check if the PoW is spent
hash := make([]byte, 8)
binary.LittleEndian.PutUint64(hash, xxhash.Sum64String(data.ProofOfWork))
_, err = mem.Exec("INSERT INTO spent (hash, expires) VALUES (?, ?)", hash, time.Now().Unix()+60)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
renderJSON(400, w, map[string]interface{}{"error": "Proof of work already spent"}, information)
return
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information)
logFunc(err.Error(), 2, information)
return
}
}
// Decode the public key
publicKey, err := base64.StdEncoding.DecodeString(data.PublicKey)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Try to insert the user
userID, err := uuid.New().MarshalBinary()
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information)
logFunc(err.Error(), 2, information)
return
}
_, err = conn.DB.Exec("INSERT INTO users (id, created, username, publicKey) VALUES ($1, $2, $3, $4)", userID, time.Now().Unix(), data.Username, publicKey)
2024-09-28 19:41:34 +01:00
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
renderJSON(409, w, map[string]interface{}{"error": "Username already taken"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09"}, information)
logFunc(err.Error(), 2, information)
}
return
}
// Create a new session
// We want the session token to be somewhat legible, so we use randomChars
// As a trade-off for this, we use a longer session token
session, err := randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "10"}, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the session
_, err = mem.Exec("INSERT INTO sessions (id, session, device) VALUES (?, ?, ?)", userID, session, r.Header.Get("User-Agent"))
2024-09-28 19:41:34 +01:00
// Return success, as well as the session token
renderJSON(200, w, map[string]interface{}{"key": session}, information)
})
router.Post("/api/loginChallenge", func(w http.ResponseWriter, r *http.Request) {
type login struct {
Username string `json:"username"`
}
var data login
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Get the id for the user
var userId []byte
err = conn.DB.QueryRow("SELECT id FROM users WHERE username = $1", data.Username).Scan(&userId)
2024-09-28 19:41:34 +01:00
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid username"}, information)
2024-09-28 19:41:34 +01:00
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "12"}, information)
logFunc(err.Error(), 2, information)
}
return
}
// Generate a new challenge
challenge, err := randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "53"}, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the challenge with one minute expiration
_, err = mem.Exec("INSERT INTO challengeResponse (challenge, userId, expires) VALUES (?, ?, ?)", challenge, userId, time.Now().Unix()+60)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "53"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the challenge
renderJSON(200, w, map[string]interface{}{"challenge": challenge}, information)
2024-09-28 19:41:34 +01:00
})
router.Post("/api/login", func(w http.ResponseWriter, r *http.Request) {
type login struct {
Username string `json:"username"`
Signature string `json:"signature"`
2024-09-28 19:41:34 +01:00
}
var data login
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Try to select the user
var userId []byte
var publicKey []byte
err = conn.DB.QueryRow("SELECT id, publicKey FROM users WHERE username = $1", data.Username).Scan(&userId, &publicKey)
2024-09-28 19:41:34 +01:00
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid username"}, information)
2024-09-28 19:41:34 +01:00
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "12"}, information)
logFunc(err.Error(), 2, information)
}
return
}
// Decode the challenge
signature, err := base64.StdEncoding.DecodeString(data.Signature)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Verify the challenge
// Select the current challenge from the database
var challenge string
err = mem.QueryRow("SELECT challenge FROM challengeResponse WHERE userId = ?", userId).Scan(&challenge)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid challenge"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "52"}, information)
logFunc(err.Error(), 2, information)
}
return
}
// Check if the challenge is correct by verifying the signature
if !ed25519.Verify(publicKey, []byte(challenge), signature) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid signature"}, information)
return
}
// Delete the challenge
_, err = mem.Exec("DELETE FROM challengeResponse WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "53"}, information)
logFunc(err.Error(), 2, information)
2024-09-28 19:41:34 +01:00
return
}
// Create a new session
// We want the session token to be somewhat legible, so we use randomChars
// As a trade-off for this, we use a longer session token
session, err := randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "13"}, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the session
_, err = mem.Exec("INSERT INTO sessions (id, session, device) VALUES (?, ?, ?)", userId, session, r.Header.Get("User-Agent"))
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "14"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return success, as well as the session token
renderJSON(200, w, map[string]interface{}{"key": session}, information)
})
router.Post("/api/userinfo", func(w http.ResponseWriter, r *http.Request) {
type userinfo struct {
Token string `json:"token"`
}
var data userinfo
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Get the username and the creation date
var username string
var created int64
err = conn.DB.QueryRow("SELECT username, created FROM users WHERE id = $1", userId).Scan(&username, &created)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "15"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the username and the creation date
renderJSON(200, w, map[string]interface{}{"username": username, "created": created}, information)
})
// This exists to make sure you get logged out immediately with an invalidated session
router.Post("/api/loggedIn", func(w http.ResponseWriter, r *http.Request) {
type loggedIn struct {
Token string `json:"token"`
}
var data loggedIn
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var session string
err = mem.QueryRow("SELECT session FROM sessions WHERE session = ?", data.Token).Scan(&session)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(401, w, map[string]interface{}{"error": "Invalid session"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "16"}, information)
logFunc(err.Error(), 2, information)
}
} else {
if session == data.Token {
// Return success
renderJSON(200, w, map[string]interface{}{"success": true}, information)
} else {
// I don't know why this happens, but it does
renderJSON(401, w, map[string]interface{}{"error": "Invalid session"}, information)
}
}
})
// Via the magic of JWTs, this support **both** access tokens and OpenID tokens. I love signing things.
// Just a shame in 15 years when Y2Q happens. That'll be a pain.
router.Get("/api/oauth/userinfo", func(w http.ResponseWriter, r *http.Request) {
// Parse the JWT
if r.Header.Get("Authorization") == "" {
renderJSON(401, w, map[string]interface{}{"error": "No token provided"}, information)
return
}
// Verify the JWT
userId, claims, ok := verifyJwt(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "), publicKey, mem)
if !ok {
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
return
}
// Check if they have the openid scope
var scopes string
err = conn.DB.QueryRow("SELECT scopes FROM oauth WHERE appId = $1", claims["aud"]).Scan(&scopes)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "17"}, information)
logFunc(err.Error(), 2, information)
return
}
// Unmarshal the scopes
var scopesArray []string
err = json.Unmarshal([]byte(scopes), &scopesArray)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "18"}, information)
logFunc(err.Error(), 2, information)
return
}
// Check if the openid scope is present
var hasOpenid bool
for _, scope := range scopesArray {
if scope == "openid" {
hasOpenid = true
break
}
}
if !hasOpenid {
renderJSON(403, w, map[string]interface{}{"error": "Missing scope"}, information)
return
}
// Get the username
var username string
err := conn.DB.QueryRow("SELECT username FROM users WHERE id = $1", userId).Scan(&username)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "19"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the username and id as sub
renderJSON(200, w, map[string]interface{}{"username": username, "sub": uuid.Must(uuid.FromBytes(userId)).String()}, information)
})
router.Post("/api/authorize", func(w http.ResponseWriter, r *http.Request) {
type authorize struct {
AppId string `json:"appId"`
PKCECode string `json:"PKCECode"`
PKCEMethod string `json:"PKCEMethod"`
RedirectUri string `json:"redirectUri"`
Nonce string `json:"nonce"`
Deny bool `json:"deny"`
Token string `json:"token"`
}
var data authorize
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Verify the AppID, redirectUri and scopes
var appId, redirectUri, scopes string
err = conn.DB.QueryRow("SELECT appId, redirectUri, scopes FROM oauth WHERE appId = $1", data.AppId).Scan(&appId, &redirectUri, &scopes)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(404, w, map[string]interface{}{"error": "App not found"}, information)
return
}
if ensureTrailingSlash(redirectUri) != ensureTrailingSlash(data.RedirectUri) || appId != data.AppId {
renderJSON(401, w, map[string]interface{}{"error": "OAuth screening failed"}, information)
return
}
// If denied, return success
if data.Deny {
renderJSON(200, w, map[string]interface{}{"success": true}, information)
return
}
// Unmarshal the scopes
var scopesArray []string
err = json.Unmarshal([]byte(scopes), &scopesArray)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "22"}, information)
logFunc(err.Error(), 2, information)
return
}
// Check if the OpenID scope is present
var hasOpenid bool
for _, scope := range scopesArray {
if scope == "openid" {
hasOpenid = true
break
}
}
// Check for the session
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Delete all ongoing logins for this user
_, err = mem.Exec("DELETE FROM logins WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "23"}, information)
logFunc(err.Error(), 2, information)
return
}
// Check for nonce
if data.Nonce == "" {
// The nonce must actually be in a URL, so we can't have pure-binary data
data.Nonce, err = randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "24"}, information)
logFunc(err.Error(), 2, information)
return
}
}
// Create the exchange code
exchangeCode, err := randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "25"}, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the login
if data.Nonce != "" {
_, err = mem.Exec("INSERT INTO logins (appId, exchangeCode, pkce, pkceMethod, openid, userId, token, nonce) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", data.AppId, exchangeCode, data.PKCECode, data.PKCEMethod, hasOpenid, userId, data.Token, data.Nonce)
} else {
_, err = mem.Exec("INSERT INTO logins (appId, exchangeCode, pkce, pkceMethod, openid, userId, token) VALUES (?, ?, ?, ?, ?, ?, ?)", data.AppId, exchangeCode, data.PKCECode, data.PKCEMethod, hasOpenid, userId, data.Token)
}
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "26"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return success
renderJSON(200, w, map[string]interface{}{"exchangeCode": exchangeCode}, information)
})
router.Post("/api/oauth/token", func(w http.ResponseWriter, r *http.Request) {
// Parse the form data
err := r.ParseForm()
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid form data"}, information)
return
}
// Declare the variables
var appId, nonce, code string
var userId []byte
var openid bool
// Check if the PKCE is present
if r.Form.Get("code_verifier") != "" {
// It is - get the PKCE code and method
var pkceCode, pkceMethod string
err = mem.QueryRow("SELECT pkce, pkceMethod, appId, userId, nonce, openid, token FROM logins WHERE exchangeCode = ?", r.Form.Get("code")).Scan(&pkceCode, &pkceMethod, &appId, &userId, &nonce, &openid, &code)
if err != nil {
renderJSON(404, w, map[string]interface{}{"error": "Code not found"}, information)
return
}
// Check the appId matches
if appId != r.Form.Get("client_id") {
renderJSON(401, w, map[string]interface{}{"error": "OAuth screening failed"}, information)
return
}
// Check the PKCE
if pkceMethod == "S256" {
if sha256Base64(r.Form.Get("code_verifier")) != pkceCode {
renderJSON(403, w, map[string]interface{}{"error": "Invalid PKCE code"}, information)
return
}
} else if pkceMethod == "plain" {
if r.Form.Get("code_verifier") != pkceCode {
renderJSON(403, w, map[string]interface{}{"error": "Invalid PKCE code"}, information)
return
}
} else {
renderJSON(400, w, map[string]interface{}{"error": "Invalid PKCE method"}, information)
return
}
} else {
// It isn't - don't get the PKCE code and method
err = mem.QueryRow("SELECT appId, userId, nonce, openid, token FROM logins WHERE exchangeCode = ?", r.Form.Get("code")).Scan(&appId, &userId, &nonce, &openid, &code)
if err != nil {
renderJSON(404, w, map[string]interface{}{"error": "Code not found"}, information)
return
}
// Check the appId matches
if appId != r.Form.Get("client_id") {
renderJSON(401, w, map[string]interface{}{"error": "OAuth screening failed"}, information)
return
}
// Verify the secret
var secret string
err = conn.DB.QueryRow("SELECT \"secret\" FROM oauth WHERE appId = $1", r.Form.Get("client_id")).Scan(&secret)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(404, w, map[string]interface{}{"error": "App not found"}, information)
return
}
if secret != r.Form.Get("client_secret") {
renderJSON(401, w, map[string]interface{}{"error": "Invalid client secret"}, information)
return
}
}
// Delete the login
_, err = mem.Exec("DELETE FROM logins WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "27"}, information)
logFunc(err.Error(), 2, information)
return
}
// Generate a new nonce if it doesn't exist
if nonce == "" {
nonce, err = randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "28"}, information)
logFunc(err.Error(), 2, information)
return
}
}
// Create the JWTs
var openIDTokenString string
if openid {
var username string
err := conn.DB.QueryRow("SELECT username FROM users WHERE id = $1", userId).Scan(&username)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "29"}, information)
logFunc(err.Error(), 2, information)
return
}
openIDTemplate := jwt.MapClaims{
"sub": uuid.Must(uuid.FromBytes(userId)).String(),
2024-09-28 19:41:34 +01:00
"iss": hostName,
"name": username,
"aud": appId,
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
"iat": time.Now().Unix(),
"auth_time": time.Now().Unix(),
"session": code,
"nonce": nonce,
"isOpenID": true,
}
openIDToken := jwt.NewWithClaims(jwt.SigningMethodEdDSA, openIDTemplate)
openIDToken.Header["kid"] = base64.StdEncoding.EncodeToString(publicKey[:8])
openIDTokenString, err = openIDToken.SignedString(privateKey)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "30"}, information)
logFunc(err.Error(), 2, information)
return
}
}
accessTokenTemplate := jwt.MapClaims{
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
"iat": time.Now().Unix(),
"session": code,
"nonce": nonce,
"aud": appId,
"isOpenID": false,
"sub": uuid.Must(uuid.FromBytes(userId)).String(),
2024-09-28 19:41:34 +01:00
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodEdDSA, accessTokenTemplate)
accessToken.Header["kid"] = base64.StdEncoding.EncodeToString(publicKey[:8])
accessTokenString, err := accessToken.SignedString(privateKey)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "31"}, information)
logFunc(err.Error(), 2, information)
return
}
// Delete all ongoing logins for this user
_, err = mem.Exec("DELETE FROM logins WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "32"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the tokens
if openid {
renderJSON(200, w, map[string]interface{}{"access_token": accessTokenString, "id_token": openIDTokenString, "token_type": "Bearer", "expires_in": 604800}, information)
} else {
renderJSON(200, w, map[string]interface{}{"access_token": accessTokenString, "token_type": "Bearer", "expires_in": 604800}, information)
}
})
router.Post("/api/oauth/remove", func(w http.ResponseWriter, r *http.Request) {
type remove struct {
Token string `json:"token"`
AppID string `json:"appId"`
}
var data remove
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Delete the oauth entry
_, err = conn.DB.Exec("DELETE FROM oauth WHERE appId = $1 AND creator = $2", data.AppID, userId)
2024-09-28 19:41:34 +01:00
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
renderJSON(404, w, map[string]interface{}{"error": "App not found"}, information)
} else {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "33"}, information)
logFunc(err.Error(), 2, information)
}
} else {
renderJSON(200, w, map[string]interface{}{"success": true}, information)
}
})
router.Post("/api/oauth/add", func(w http.ResponseWriter, r *http.Request) {
// Conveniently, we use this one for ISB as well, so we can re-use the struct
var data authLibrary.OAuthInformation
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Generate a new secret
// It must be able to be sent via JSON, so we can't have pure-binary data
secret, err := randomChars(512)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "34"}, information)
logFunc(err.Error(), 2, information)
return
}
// Generate a new appId
// It must be able to be sent via JSON, so we can't have pure-binary data
appId, err := randomChars(32)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "35"}, information)
logFunc(err.Error(), 2, information)
return
}
// Validate the scopes
var clientKeyShare bool
for _, scope := range data.Scopes {
if scope != "openid" && scope != "clientKeyShare" {
renderJSON(400, w, map[string]interface{}{"error": "Invalid scope"}, information)
return
} else {
if scope == "clientKeyShare" {
clientKeyShare = true
} else if scope != "openid" {
logFunc("An impossible logic error has occurred, please move away from radiation or use ECC RAM", 1, information)
renderJSON(400, w, map[string]interface{}{"error": "Invalid scope"}, information)
return
}
}
}
// Marshal the scopes
scopes, err := json.Marshal(data.Scopes)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "36"}, information)
logFunc(err.Error(), 2, information)
return
}
// Insert the oauth entry
if clientKeyShare {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ($1, $2, $3, $4, $5, $6, $7)", appId, secret, userId, data.Name, data.RedirectUri, scopes, data.KeyShareUri)
2024-09-28 19:41:34 +01:00
} else {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes) VALUES ($1, $2, $3, $4, $5, $6)", appId, secret, userId, data.Name, data.RedirectUri, scopes)
2024-09-28 19:41:34 +01:00
}
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "37"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return the appId and secret
renderJSON(200, w, map[string]interface{}{"appId": appId, "key": secret}, information)
})
router.Post("/api/oauth/list", func(w http.ResponseWriter, r *http.Request) {
type list struct {
Token string `json:"token"`
}
var data list
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Get the apps
rows, err := conn.DB.Query("SELECT appId, name, redirectUri, scopes, keyShareUri FROM oauth WHERE creator = $1", userId)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "38"}, information)
logFunc(err.Error(), 2, information)
return
}
var dataOut []map[string]interface{}
for rows.Next() {
var appId, name, redirectUri, scopes, keyShareUri string
err = rows.Scan(&appId, &name, &redirectUri, &scopes, &keyShareUri)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "39"}, information)
logFunc(err.Error(), 2, information)
return
}
// Marshal the scopes
var scopesArray []string
err = json.Unmarshal([]byte(scopes), &scopesArray)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "40"}, information)
logFunc(err.Error(), 2, information)
return
}
dataOut = append(dataOut, map[string]interface{}{
"appId": appId,
"name": name,
"redirectUri": redirectUri,
"scopes": scopesArray,
"keyShareUri": keyShareUri,
})
}
err = rows.Err()
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "41"}, information)
logFunc(err.Error(), 2, information)
return
}
// Close the rows
err = rows.Close()
if err != nil {
// Memory leak, but we can't do anything about it
logFunc(err.Error(), 1, information)
}
// Return the apps
renderJSON(200, w, map[string]interface{}{
"apps": dataOut,
}, information)
})
router.Post("/api/deleteAccount", func(w http.ResponseWriter, r *http.Request) {
type deleteAccount struct {
Token string `json:"token"`
}
var data deleteAccount
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Delete the user
_, err = conn.DB.Exec("DELETE FROM users WHERE id = $1", userId)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "42"}, information)
logFunc(err.Error(), 2, information)
return
}
// Delete the user's sessions
_, err = mem.Exec("DELETE FROM sessions WHERE id = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "43"}, information)
logFunc(err.Error(), 2, information)
return
}
// Delete the user's oauth entries
_, err = conn.DB.Exec("DELETE FROM oauth WHERE creator = ?", userId)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "44"}, information)
logFunc(err.Error(), 2, information)
return
}
// Delete the user's active logins
_, err = mem.Exec("DELETE FROM logins WHERE userId = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "45"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return success
renderJSON(200, w, map[string]interface{}{"success": true}, information)
})
router.Post("/api/session/list", func(w http.ResponseWriter, r *http.Request) {
type list struct {
Token string `json:"token"`
}
var data list
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Get the sessions
rows, err := mem.Query("SELECT session, device FROM sessions WHERE id = ?", userId)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "46"}, information)
logFunc(err.Error(), 2, information)
return
}
var dataOut []map[string]interface{}
for rows.Next() {
var session, device string
err = rows.Scan(&session, &device)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "47"}, information)
logFunc(err.Error(), 2, information)
return
}
dataOut = append(dataOut, map[string]interface{}{
"session": session,
"device": device,
})
}
err = rows.Err()
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "48"}, information)
logFunc(err.Error(), 2, information)
return
}
// Close the rows
err = rows.Close()
if err != nil {
// Memory leak, but we can't do anything about it
logFunc(err.Error(), 1, information)
}
// Return the sessions
renderJSON(200, w, map[string]interface{}{
"sessions": dataOut,
}, information)
})
router.Post("/api/session/remove", func(w http.ResponseWriter, r *http.Request) {
type remove struct {
Token string `json:"token"`
Session string `json:"session"`
}
var data remove
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the session exists
var userId []byte
err = mem.QueryRow("SELECT id FROM sessions WHERE session = ?", data.Token).Scan(&userId)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid session"}, information)
return
}
// Delete the session
_, err = mem.Exec("DELETE FROM sessions WHERE id = $1 AND session = ?", userId, data.Session)
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "49"}, information)
logFunc(err.Error(), 2, information)
return
}
// Return success
renderJSON(200, w, map[string]interface{}{"success": true}, information)
})
router.Post("/api/listUsers", func(w http.ResponseWriter, r *http.Request) {
type listUsers struct {
AdminKey string `json:"adminKey"`
}
var data listUsers
err = json.NewDecoder(r.Body).Decode(&data)
if err != nil {
renderJSON(400, w, map[string]interface{}{"error": "Invalid JSON"}, information)
return
}
// Check if the admin key is correct
// Finally, I get to use the admin key
// In previous versions, the admin key was very important - it handled server cookie-based secrets
// as well as JWTs - but now it's just a glorified password
if data.AdminKey != adminKey {
renderJSON(401, w, map[string]interface{}{"error": "Invalid admin key"}, information)
return
}
// Get the users
rows, err := conn.DB.Query("SELECT id, username, created FROM users")
2024-09-28 19:41:34 +01:00
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "50"}, information)
logFunc(err.Error(), 2, information)
return
}
var dataOut []map[string]interface{}
for rows.Next() {
var id []byte
var username string
var created int64
err = rows.Scan(&id, &username, &created)
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "51"}, information)
logFunc(err.Error(), 2, information)
return
}
dataOut = append(dataOut, map[string]interface{}{
// Also finally get a chance to represent UUIDs as strings. Everywhere else I just use them as bytes.
"id": uuid.Must(uuid.FromBytes(id)).String(),
"username": username,
"created": created,
})
}
err = rows.Err()
if err != nil {
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "52"}, information)
logFunc(err.Error(), 2, information)
return
}
// Close the rows
err = rows.Close()
if err != nil {
// Memory leak, but we can't do anything about it
logFunc(err.Error(), 1, information)
}
// Return the users
renderJSON(200, w, map[string]interface{}{
"users": dataOut,
}, information)
})
router.Get("/.well-known/keys.json", func(w http.ResponseWriter, r *http.Request) {
// Return the public key
renderJSON(200, w, map[string]interface{}{
"keys": []map[string]interface{}{
{
"kty": "OKP",
"alg": "EdDSA",
"use": "sig",
"kid": base64.StdEncoding.EncodeToString(publicKey[:8]),
"x": base64.RawURLEncoding.EncodeToString(publicKey),
"crv": "Ed25519",
},
},
}, information)
})
go func() {
for {
// Sleep for half an hour
time.Sleep(time.Minute * 30)
// Delete everything in the spent and challenge-response tables that has expired
2024-09-28 19:41:34 +01:00
affected, err := mem.Exec("DELETE FROM spent WHERE expires < ?", time.Now().Unix())
if err != nil {
logFunc(err.Error(), 1, information)
} else {
affectedCount, err := affected.RowsAffected()
if err != nil {
logFunc(err.Error(), 1, information)
} else {
affected, err := mem.Exec("DELETE FROM challengeResponse WHERE expires < ?", time.Now().Unix())
if err != nil {
logFunc(err.Error(), 1, information)
} else {
affectedCount2, err := affected.RowsAffected()
if err != nil {
logFunc(err.Error(), 1, information)
} else {
logFunc("Cleanup complete, deleted "+strconv.FormatInt(affectedCount+affectedCount2, 10)+" entries", 0, information)
}
}
2024-09-28 19:41:34 +01:00
}
}
}
}()
go func() {
for {
// Wait for a message
message := <-information.Inbox
if message.ServiceID != uuid.MustParse("00000000-0000-0000-0000-000000000001") {
// Check the message type
switch message.MessageType {
case 0:
// A service would like to know our hostname
// Send it to them
2024-09-28 19:41:34 +01:00
information.Outbox <- library.InterServiceMessage{
MessageType: 0,
2024-09-28 19:41:34 +01:00
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: hostName,
2024-09-28 19:41:34 +01:00
SentAt: time.Now(),
}
case 1:
// A service would like to register a new OAuth entry
// Generate a new secret
// It must be able to be sent via JSON, so we can't have pure-binary data
secret, err := randomChars(512)
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "36",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
2024-09-28 19:41:34 +01:00
}
// Generate a new appId
// It must be able to be sent via JSON, so we can't have pure-binary data
appId, err := randomChars(32)
if err != nil {
2024-09-28 19:41:34 +01:00
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
2024-09-28 19:41:34 +01:00
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "37",
2024-09-28 19:41:34 +01:00
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
2024-09-28 19:41:34 +01:00
return
}
// Validate the scopes
var clientKeyShare bool
for _, scope := range message.Message.(authLibrary.OAuthInformation).Scopes {
if scope != "openid" && scope != "clientKeyShare" {
2024-09-28 19:41:34 +01:00
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "Invalid scope",
SentAt: time.Now(),
}
return
} else {
if scope == "clientKeyShare" {
clientKeyShare = true
} else if scope != "openid" {
logFunc("An impossible logic error has occurred, please move away from radiation or use ECC RAM", 1, information)
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "Invalid scope",
SentAt: time.Now(),
}
return
}
2024-09-28 19:41:34 +01:00
}
}
// Marshal the scopes
scopes, err := json.Marshal(message.Message.(authLibrary.OAuthInformation).Scopes)
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "38",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
// Insert the oauth entry
if clientKeyShare {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ($1, $2, $3, $4, $5, $6, $7)", appId, secret, serviceIDBytes, message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes, message.Message.(authLibrary.OAuthInformation).KeyShareUri)
} else {
_, err = conn.DB.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes) VALUES ($1, $2, $3, $4, $5, $6)", appId, secret, serviceIDBytes, message.Message.(authLibrary.OAuthInformation).Name, message.Message.(authLibrary.OAuthInformation).RedirectUri, scopes)
}
if err != nil {
information.Outbox <- library.InterServiceMessage{
MessageType: 1,
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: "39",
SentAt: time.Now(),
}
logFunc(err.Error(), 2, information)
return
}
// Return the appId and secret
2024-09-28 19:41:34 +01:00
information.Outbox <- library.InterServiceMessage{
MessageType: 0,
2024-09-28 19:41:34 +01:00
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: authLibrary.OAuthResponse{
AppID: appId,
SecretKey: secret,
},
SentAt: time.Now(),
2024-09-28 19:41:34 +01:00
}
case 2:
// A service would like to have the public key
// Send it to them
2024-09-28 19:41:34 +01:00
information.Outbox <- library.InterServiceMessage{
MessageType: 2,
2024-09-28 19:41:34 +01:00
ServiceID: ServiceInformation.ServiceID,
ForServiceID: message.ServiceID,
Message: publicKey,
2024-09-28 19:41:34 +01:00
SentAt: time.Now(),
}
}
2024-09-28 19:41:34 +01:00
}
}
}()
// Report a successful activation
information.Outbox <- library.InterServiceMessage{
ServiceID: ServiceInformation.ServiceID,
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Activation service
MessageType: 0,
SentAt: time.Now(),
Message: router,
}
2024-09-28 19:41:34 +01:00
}