mirror of
https://github.com/1f349/lavender.git
synced 2024-11-09 22:32:48 +00:00
Finish up working flow and setup test client
This commit is contained in:
parent
d772d14041
commit
7c3b36c9ae
@ -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"`
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -2,20 +2,17 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{.ServiceName}}</title>
|
||||
</head>
|
||||
<script>
|
||||
let loginData = {target:{{.TargetOrigin}}, message:{{.LoginData}}};
|
||||
document.addEventListener("load", function () {
|
||||
postMessage(loginData.message, loginData.target);
|
||||
setTimeout(function () {
|
||||
window.close();
|
||||
}, 2000);
|
||||
let loginData = {target:{{.TargetOrigin}}, message:{{.TargetMessage}}};
|
||||
window.addEventListener("load", function () {
|
||||
window.opener.postMessage(loginData.message, loginData.target);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{.ServiceName}}</h1>
|
||||
</header>
|
||||
<main>Loading...</main>
|
||||
<main id="mainBody">Loading...</main>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
type HttpServer struct {
|
||||
r *httprouter.Router
|
||||
baseUrl string
|
||||
serviceName string
|
||||
manager *issuer.Manager
|
||||
signer mjwt.Signer
|
||||
flowState *cache.Cache[string, flowStateData]
|
||||
@ -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,14 +39,16 @@ 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,
|
||||
serviceName: serviceName,
|
||||
manager: manager,
|
||||
signer: signer,
|
||||
flowState: cache.New[string, flowStateData](),
|
||||
services: services,
|
||||
}
|
||||
|
||||
|
75
test-client/index.html
Normal file
75
test-client/index.html
Normal 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
2
test-client/run.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python3 -m http.server 2020
|
Loading…
Reference in New Issue
Block a user