mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 07:34:06 +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 {
|
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"`
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -2,20 +2,17 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{.ServiceName}}</title>
|
<title>{{.ServiceName}}</title>
|
||||||
|
<script>
|
||||||
|
let loginData = {target:{{.TargetOrigin}}, message:{{.TargetMessage}}};
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
window.opener.postMessage(loginData.message, loginData.target);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<script>
|
|
||||||
let loginData = {target:{{.TargetOrigin}}, message:{{.LoginData}}};
|
|
||||||
document.addEventListener("load", function () {
|
|
||||||
postMessage(loginData.message, loginData.target);
|
|
||||||
setTimeout(function () {
|
|
||||||
window.close();
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>{{.ServiceName}}</h1>
|
<h1>{{.ServiceName}}</h1>
|
||||||
</header>
|
</header>
|
||||||
<main>Loading...</main>
|
<main id="mainBody">Loading...</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -12,12 +12,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HttpServer struct {
|
type HttpServer struct {
|
||||||
r *httprouter.Router
|
r *httprouter.Router
|
||||||
baseUrl string
|
baseUrl string
|
||||||
manager *issuer.Manager
|
serviceName string
|
||||||
signer mjwt.Signer
|
manager *issuer.Manager
|
||||||
flowState *cache.Cache[string, flowStateData]
|
signer mjwt.Signer
|
||||||
services map[string]struct{}
|
flowState *cache.Cache[string, flowStateData]
|
||||||
|
services map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type flowStateData struct {
|
type flowStateData struct {
|
||||||
@ -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,15 +39,17 @@ 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,
|
||||||
manager: manager,
|
serviceName: serviceName,
|
||||||
signer: signer,
|
manager: manager,
|
||||||
services: services,
|
signer: signer,
|
||||||
|
flowState: cache.New[string, flowStateData](),
|
||||||
|
services: services,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET("/", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
r.GET("/", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
||||||
|
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