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 {
Listen string `json:"listen"`
BaseUrl string `json:"base_url"`
ServiceName string `json:"service_name"`
Issuer string `json:"issuer"`
SsoServices []loginServiceManager.SsoConfig `json:"sso_services"`
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)
}
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)
go utils.RunBackgroundHttp("HTTP", srv)

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,13 @@ import (
)
type HttpServer struct {
r *httprouter.Router
baseUrl string
manager *issuer.Manager
signer mjwt.Signer
flowState *cache.Cache[string, flowStateData]
services map[string]struct{}
r *httprouter.Router
baseUrl string
serviceName string
manager *issuer.Manager
signer mjwt.Signer
flowState *cache.Cache[string, flowStateData]
services map[string]struct{}
}
type flowStateData struct {
@ -25,7 +26,7 @@ type flowStateData struct {
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()
// remove last slash from baseUrl
@ -38,15 +39,17 @@ func NewHttpServer(listen, baseUrl string, clients []utils.JsonUrl, manager *iss
services := make(map[string]struct{})
for _, i := range clients {
services[i.Host] = struct{}{}
services[i.String()] = struct{}{}
}
hs := &HttpServer{
r: r,
baseUrl: baseUrl,
manager: manager,
signer: signer,
services: services,
r: r,
baseUrl: baseUrl,
serviceName: serviceName,
manager: manager,
signer: signer,
flowState: cache.New[string, flowStateData](),
services: services,
}
r.GET("/", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {

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