Finish up working flow and setup test client

This commit is contained in:
Melon 2023-10-04 21:53:20 +01:00
parent d772d14041
commit 7c3b36c9ae
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 124 additions and 32 deletions

View File

@ -8,6 +8,7 @@ import (
type startUpConfig struct { type startUpConfig struct {
Listen string `json:"listen"` Listen string `json:"listen"`
BaseUrl string `json:"base_url"` BaseUrl string `json:"base_url"`
ServiceName string `json:"service_name"`
Issuer string `json:"issuer"` Issuer string `json:"issuer"`
SsoServices []loginServiceManager.SsoConfig `json:"sso_services"` SsoServices []loginServiceManager.SsoConfig `json:"sso_services"`
AllowedClients []utils.JsonUrl `json:"allowed_clients"` AllowedClients []utils.JsonUrl `json:"allowed_clients"`

View File

@ -77,7 +77,7 @@ func normalLoad(startUp startUpConfig, wd string) {
log.Fatal("[Lavender] Failed to create SSO service manager: ", err) log.Fatal("[Lavender] Failed to create SSO service manager: ", err)
} }
srv := server.NewHttpServer(startUp.Listen, startUp.BaseUrl, startUp.AllowedClients, manager, mSign) srv := server.NewHttpServer(startUp.Listen, startUp.BaseUrl, startUp.ServiceName, startUp.AllowedClients, manager, mSign)
log.Printf("[Lavender] Starting HTTP server on '%s'\n", srv.Addr) log.Printf("[Lavender] Starting HTTP server on '%s'\n", srv.Addr)
go utils.RunBackgroundHttp("HTTP", srv) go utils.RunBackgroundHttp("HTTP", srv)

View File

@ -46,6 +46,7 @@ func (s SsoConfig) FetchConfig() (*WellKnownOIDC, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.Config = s
c.OAuth2Config = oauth2.Config{ c.OAuth2Config = oauth2.Config{
ClientID: c.Config.Client.ID, ClientID: c.Config.Client.ID,
ClientSecret: c.Config.Client.Secret, ClientSecret: c.Config.Client.Secret,

View File

@ -2,20 +2,17 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>{{.ServiceName}}</title> <title>{{.ServiceName}}</title>
</head> <script>
<script> let loginData = {target:{{.TargetOrigin}}, message:{{.TargetMessage}}};
let loginData = {target:{{.TargetOrigin}}, message:{{.LoginData}}}; window.addEventListener("load", function () {
document.addEventListener("load", function () { window.opener.postMessage(loginData.message, loginData.target);
postMessage(loginData.message, loginData.target);
setTimeout(function () {
window.close();
}, 2000);
}); });
</script> </script>
</head>
<body> <body>
<header> <header>
<h1>{{.ServiceName}}</h1> <h1>{{.ServiceName}}</h1>
</header> </header>
<main>Loading...</main> <main id="mainBody">Loading...</main>
</body> </body>
</html> </html>

View File

@ -8,11 +8,11 @@
<h1>{{.ServiceName}}</h1> <h1>{{.ServiceName}}</h1>
</header> </header>
<main> <main>
<form method="POST" action="/login"> <form method="POST" action="/popup">
<input type="hidden" name="return" value="{{.Return}}"/> <input type="hidden" name="origin" value="{{.Origin}}"/>
<div> <div>
<label for="field_username">User Name:</label> <label for="field_loginname">Login Name:</label>
<input type="text" name="username" id="field_username" required/> <input type="text" name="loginname" id="field_loginname" required/>
</div> </div>
<button type="submit">Continue</button> <button type="submit">Continue</button>
</form> </form>

View File

@ -1,10 +1,13 @@
package server package server
import ( import (
"context"
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"golang.org/x/oauth2"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@ -28,6 +31,7 @@ func init() {
log.Fatal("flow.go: Failed to parse flow popup HTML:", err) log.Fatal("flow.go: Failed to parse flow popup HTML:", err)
} }
flowPopupTemplate = pageParse flowPopupTemplate = pageParse
pageParse, err = template.New("pages").Parse(flowCallbackHtml) pageParse, err = template.New("pages").Parse(flowCallbackHtml)
if err != nil { if err != nil {
log.Fatal("flow.go: Failed to parse flow callback HTML:", err) log.Fatal("flow.go: Failed to parse flow callback HTML:", err)
@ -37,7 +41,7 @@ func init() {
func (h *HttpServer) flowPopup(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { func (h *HttpServer) flowPopup(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
err := flowPopupTemplate.Execute(rw, map[string]any{ err := flowPopupTemplate.Execute(rw, map[string]any{
"ServiceName": flowPopupTemplate, "ServiceName": h.serviceName,
"Origin": req.URL.Query().Get("origin"), "Origin": req.URL.Query().Get("origin"),
}) })
if err != nil { if err != nil {
@ -46,7 +50,8 @@ func (h *HttpServer) flowPopup(rw http.ResponseWriter, req *http.Request, _ http
} }
func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) { func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
login := h.manager.FindServiceFromLogin(req.PostFormValue("username")) loginName := req.PostFormValue("loginname")
login := h.manager.FindServiceFromLogin(loginName)
if login == nil { if login == nil {
http.Error(rw, "No login service defined for this username", http.StatusBadRequest) http.Error(rw, "No login service defined for this username", http.StatusBadRequest)
return return
@ -68,7 +73,7 @@ func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _
// generate oauth2 config and redirect to authorize URL // generate oauth2 config and redirect to authorize URL
oa2conf := login.OAuth2Config oa2conf := login.OAuth2Config
oa2conf.RedirectURL = h.baseUrl + "/callback" oa2conf.RedirectURL = h.baseUrl + "/callback"
nextUrl := oa2conf.AuthCodeURL(state) nextUrl := oa2conf.AuthCodeURL(state, oauth2.SetAuthURLParam("login_name", loginName))
http.Redirect(rw, req, nextUrl, http.StatusFound) http.Redirect(rw, req, nextUrl, http.StatusFound)
} }
@ -92,14 +97,18 @@ func (h *HttpServer) flowCallback(rw http.ResponseWriter, req *http.Request, _ h
return return
} }
exchange, err := v.sso.OAuth2Config.Exchange(req.Context(), q.Get("code")) oa2conf := v.sso.OAuth2Config
oa2conf.RedirectURL = h.baseUrl + "/callback"
exchange, err := oa2conf.Exchange(context.Background(), q.Get("code"))
if err != nil { if err != nil {
fmt.Println("Failed exchange:", err)
http.Error(rw, "Failed to exchange code", http.StatusInternalServerError) http.Error(rw, "Failed to exchange code", http.StatusInternalServerError)
return return
} }
client := v.sso.OAuth2Config.Client(req.Context(), exchange) client := v.sso.OAuth2Config.Client(req.Context(), exchange)
v2, err := client.Get(v.sso.UserInfoEndpoint) v2, err := client.Get(v.sso.UserInfoEndpoint)
if err != nil { if err != nil {
fmt.Println("Failed to get userinfo:", err)
http.Error(rw, "Failed to get userinfo", http.StatusInternalServerError) http.Error(rw, "Failed to get userinfo", http.StatusInternalServerError)
return return
} }
@ -110,11 +119,15 @@ func (h *HttpServer) flowCallback(rw http.ResponseWriter, req *http.Request, _ h
} }
var v3 any var v3 any
if json.NewDecoder(v2.Body).Decode(&v3) != nil { if json.NewDecoder(v2.Body).Decode(&v3) != nil {
fmt.Println("Failed to decode userinfo:", err)
http.Error(rw, "Failed to decode userinfo JSON", http.StatusInternalServerError) http.Error(rw, "Failed to decode userinfo JSON", http.StatusInternalServerError)
return return
} }
// TODO: generate signed mjwt object
_ = flowCallbackTemplate.Execute(rw, map[string]any{ _ = flowCallbackTemplate.Execute(rw, map[string]any{
"ServiceName": h.serviceName,
"TargetOrigin": v.targetOrigin, "TargetOrigin": v.targetOrigin,
"TargetMessage": v3, "TargetMessage": v3,
}) })

View File

@ -14,6 +14,7 @@ import (
type HttpServer struct { type HttpServer struct {
r *httprouter.Router r *httprouter.Router
baseUrl string baseUrl string
serviceName string
manager *issuer.Manager manager *issuer.Manager
signer mjwt.Signer signer mjwt.Signer
flowState *cache.Cache[string, flowStateData] flowState *cache.Cache[string, flowStateData]
@ -25,7 +26,7 @@ type flowStateData struct {
targetOrigin string targetOrigin string
} }
func NewHttpServer(listen, baseUrl string, clients []utils.JsonUrl, manager *issuer.Manager, signer mjwt.Signer) *http.Server { func NewHttpServer(listen, baseUrl, serviceName string, clients []utils.JsonUrl, manager *issuer.Manager, signer mjwt.Signer) *http.Server {
r := httprouter.New() r := httprouter.New()
// remove last slash from baseUrl // remove last slash from baseUrl
@ -38,14 +39,16 @@ func NewHttpServer(listen, baseUrl string, clients []utils.JsonUrl, manager *iss
services := make(map[string]struct{}) services := make(map[string]struct{})
for _, i := range clients { for _, i := range clients {
services[i.Host] = struct{}{} services[i.String()] = struct{}{}
} }
hs := &HttpServer{ hs := &HttpServer{
r: r, r: r,
baseUrl: baseUrl, baseUrl: baseUrl,
serviceName: serviceName,
manager: manager, manager: manager,
signer: signer, signer: signer,
flowState: cache.New[string, flowStateData](),
services: services, services: services,
} }

75
test-client/index.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Client</title>
<script>
var currentLoginPopup = null;
window.addEventListener("message", function (event) {
if (event.origin !== "http:\/\/localhost:9090") return;
if (isObject(event.data)) {
document.getElementById("someTextArea").textContent = JSON.stringify(event.data, null, 2);
if(currentLoginPopup) currentLoginPopup.close();
return;
}
alert("Failed to log user in: the login data was probably corrupted");
});
function isObject(obj) {
return obj != null && obj.constructor.name === "Object"
}
function popupCenterScreen(url, title, w, h, focus) {
const top = (screen.availHeight - h) / 4, left = (screen.availWidth - w) / 2;
const popup = openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
if (focus === true && window.focus) popup.focus();
return popup;
}
function openWindow(url, winnm, options) {
var wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
var wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
var top = 0, left = 0;
var result;
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
if (options) {
options = options.replace("top=" + top, "top=" + (parseInt(top) + wTop));
options = options.replace("left=" + left, "left=" + (parseInt(left) + wLeft));
w = window.open(url, winnm, options);
} else w = window.open(url, winnm);
return w;
}
function firstAvailableValue(arr) {
for (var i = 0; i < arr.length; i++)
if (typeof arr[i] != 'undefined')
return arr[i];
}
function doThisThing() {
if(currentLoginPopup) currentLoginPopup.close();
currentLoginPopup = popupCenterScreen('http://localhost:9090/popup?origin='+encodeURIComponent("http://localhost:2020"), 'Login with Lavender', 500, 500, false);
}
</script>
<style>
#someTextArea {
width: 400px;
height: 400px;
}
</style>
</head>
<body>
<header>
<h1>Test Client</h1>
</header>
<main>
<div>
<button onclick="doThisThing();">Login</button>
</div>
<div>
<textarea id="someTextArea"></textarea>
</div>
</main>
</body>
</html>

2
test-client/run.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
python3 -m http.server 2020