mirror of
https://github.com/1f349/lavender.git
synced 2024-12-22 07:34:06 +00:00
Write flow popup tests
This commit is contained in:
parent
f5f003298e
commit
1280c30c5e
@ -30,6 +30,17 @@ func NewManager(services []SsoConfig) (*Manager, error) {
|
|||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewManagerForTests(services []WellKnownOIDC) *Manager {
|
||||||
|
l := &Manager{m: make(map[string]*WellKnownOIDC, len(services))}
|
||||||
|
for _, i := range services {
|
||||||
|
if !isValidNamespace.MatchString(i.Config.Namespace) {
|
||||||
|
panic("Invalid namespace in tests")
|
||||||
|
}
|
||||||
|
l.m[i.Config.Namespace] = &i
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
func (l *Manager) CheckNamespace(namespace string) bool {
|
func (l *Manager) CheckNamespace(namespace string) bool {
|
||||||
_, ok := l.m[namespace]
|
_, ok := l.m[namespace]
|
||||||
return ok
|
return ok
|
||||||
|
@ -17,13 +17,15 @@ var httpGet = http.Get
|
|||||||
// SsoConfig is the base URL for an OAUTH/OPENID/SSO login service
|
// SsoConfig is the base URL for an OAUTH/OPENID/SSO login service
|
||||||
// The path `/.well-known/openid-configuration` should be available
|
// The path `/.well-known/openid-configuration` should be available
|
||||||
type SsoConfig struct {
|
type SsoConfig struct {
|
||||||
Addr utils.JsonUrl `json:"addr"` // https://login.example.com
|
Addr utils.JsonUrl `json:"addr"` // https://login.example.com
|
||||||
Namespace string `json:"namespace"` // example.com
|
Namespace string `json:"namespace"` // example.com
|
||||||
Client struct {
|
Client SsoConfigClient `json:"client"`
|
||||||
ID string `json:"id"`
|
}
|
||||||
Secret string `json:"secret"`
|
|
||||||
Scopes []string `json:"scopes"`
|
type SsoConfigClient struct {
|
||||||
} `json:"client"`
|
ID string `json:"id"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SsoConfig) FetchConfig() (*WellKnownOIDC, error) {
|
func (s SsoConfig) FetchConfig() (*WellKnownOIDC, error) {
|
||||||
|
@ -12,20 +12,20 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var uuidNewStringState = uuid.NewString
|
||||||
|
var uuidNewStringAti = uuid.NewString
|
||||||
|
var uuidNewStringRti = uuid.NewString
|
||||||
|
|
||||||
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 := pages.FlowTemplates.Execute(rw, map[string]any{
|
pages.RenderPageTemplate(rw, "flow-popup", map[string]any{
|
||||||
"ServiceName": h.conf.ServiceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"Origin": req.URL.Query().Get("origin"),
|
"Origin": req.URL.Query().Get("origin"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to render page: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
||||||
@ -43,7 +43,7 @@ func (h *HttpServer) flowPopupPost(rw http.ResponseWriter, req *http.Request, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save state for use later
|
// save state for use later
|
||||||
state := login.Config.Namespace + ":" + uuid.NewString()
|
state := login.Config.Namespace + ":" + uuidNewStringState()
|
||||||
h.flowState.Set(state, flowStateData{
|
h.flowState.Set(state, flowStateData{
|
||||||
login,
|
login,
|
||||||
targetOrigin,
|
targetOrigin,
|
||||||
@ -117,7 +117,7 @@ func (h *HttpServer) flowCallback(rw http.ResponseWriter, req *http.Request, _ h
|
|||||||
|
|
||||||
ps := claims.NewPermStorage()
|
ps := claims.NewPermStorage()
|
||||||
nsSub := sub + "@" + v.sso.Config.Namespace
|
nsSub := sub + "@" + v.sso.Config.Namespace
|
||||||
ati := uuid.NewString()
|
ati := uuidNewStringAti()
|
||||||
accessToken, err := h.signer.GenerateJwt(nsSub, ati, jwt.ClaimStrings{aud}, 15*time.Minute, auth.AccessTokenClaims{
|
accessToken, err := h.signer.GenerateJwt(nsSub, ati, jwt.ClaimStrings{aud}, 15*time.Minute, auth.AccessTokenClaims{
|
||||||
Perms: ps,
|
Perms: ps,
|
||||||
})
|
})
|
||||||
@ -126,13 +126,13 @@ func (h *HttpServer) flowCallback(rw http.ResponseWriter, req *http.Request, _ h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshToken, err := h.signer.GenerateJwt(nsSub, uuid.NewString(), jwt.ClaimStrings{aud}, 15*time.Minute, auth.RefreshTokenClaims{AccessTokenId: ati})
|
refreshToken, err := h.signer.GenerateJwt(nsSub, uuidNewStringRti(), jwt.ClaimStrings{aud}, 15*time.Minute, auth.RefreshTokenClaims{AccessTokenId: ati})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(rw, "Error generating refresh token", http.StatusInternalServerError)
|
http.Error(rw, "Error generating refresh token", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = pages.FlowTemplates.Execute(rw, map[string]any{
|
pages.RenderPageTemplate(rw, "flow-callback", map[string]any{
|
||||||
"ServiceName": h.conf.ServiceName,
|
"ServiceName": h.conf.ServiceName,
|
||||||
"TargetOrigin": v.targetOrigin,
|
"TargetOrigin": v.targetOrigin,
|
||||||
"TargetMessage": v3,
|
"TargetMessage": v3,
|
||||||
|
@ -1 +1,144 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/1f349/cache"
|
||||||
|
"github.com/1f349/lavender/issuer"
|
||||||
|
"github.com/1f349/lavender/server/pages"
|
||||||
|
"github.com/1f349/lavender/utils"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const lavenderDomain = "http://localhost:0"
|
||||||
|
const clientAppDomain = "http://localhost:1"
|
||||||
|
const loginDomain = "http://localhost:2"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
err := pages.LoadPages("")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlowPopup(t *testing.T) {
|
||||||
|
h := HttpServer{conf: Conf{ServiceName: "Test Service Name"}}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/popup?"+url.Values{"origin": []string{clientAppDomain}}.Encode(), nil)
|
||||||
|
h.flowPopup(rec, req, httprouter.Params{})
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, fmt.Sprintf(`<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Test Service Name</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Test Service Name</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<form method="POST" action="/popup">
|
||||||
|
<input type="hidden" name="origin" value="%s"/>
|
||||||
|
<div>
|
||||||
|
<label for="field_loginname">Login Name:</label>
|
||||||
|
<input type="text" name="loginname" id="field_loginname" required/>
|
||||||
|
</div>
|
||||||
|
<button type="submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, clientAppDomain), rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlowPopupPost(t *testing.T) {
|
||||||
|
manager := issuer.NewManagerForTests([]issuer.WellKnownOIDC{
|
||||||
|
{
|
||||||
|
Config: issuer.SsoConfig{
|
||||||
|
Addr: utils.JsonUrl{},
|
||||||
|
Namespace: "example.com",
|
||||||
|
Client: issuer.SsoConfigClient{
|
||||||
|
ID: "test-id",
|
||||||
|
Secret: "test-secret",
|
||||||
|
Scopes: []string{"openid"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Issuer: "https://example.com",
|
||||||
|
AuthorizationEndpoint: loginDomain + "/authorize",
|
||||||
|
TokenEndpoint: loginDomain + "/token",
|
||||||
|
UserInfoEndpoint: loginDomain + "/userinfo",
|
||||||
|
ResponseTypesSupported: nil,
|
||||||
|
ScopesSupported: nil,
|
||||||
|
ClaimsSupported: nil,
|
||||||
|
GrantTypesSupported: nil,
|
||||||
|
OAuth2Config: oauth2.Config{
|
||||||
|
ClientID: "test-id",
|
||||||
|
ClientSecret: "test-secret",
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: loginDomain + "/authorize",
|
||||||
|
TokenURL: loginDomain + "/token",
|
||||||
|
AuthStyle: oauth2.AuthStyleInHeader,
|
||||||
|
},
|
||||||
|
Scopes: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
h := HttpServer{
|
||||||
|
r: nil,
|
||||||
|
conf: Conf{BaseUrl: lavenderDomain},
|
||||||
|
manager: manager,
|
||||||
|
flowState: cache.New[string, flowStateData](),
|
||||||
|
services: map[string]struct{}{
|
||||||
|
clientAppDomain: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test no login service error
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/popup", strings.NewReader(url.Values{
|
||||||
|
"loginname": []string{"test@missing.example.com"},
|
||||||
|
"origin": []string{clientAppDomain},
|
||||||
|
}.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
h.flowPopupPost(rec, req, httprouter.Params{})
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
|
assert.Equal(t, "No login service defined for this username\n", rec.Body.String())
|
||||||
|
|
||||||
|
// test invalid target origin error
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/popup", strings.NewReader(url.Values{
|
||||||
|
"loginname": []string{"test@example.com"},
|
||||||
|
"origin": []string{"http://localhost:1010"},
|
||||||
|
}.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
h.flowPopupPost(rec, req, httprouter.Params{})
|
||||||
|
assert.Equal(t, http.StatusBadRequest, rec.Code)
|
||||||
|
assert.Equal(t, "Invalid target origin\n", rec.Body.String())
|
||||||
|
|
||||||
|
// test successful request
|
||||||
|
nextState := uuid.NewString()
|
||||||
|
uuidNewStringState = func() string { return nextState }
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/popup", strings.NewReader(url.Values{
|
||||||
|
"loginname": []string{"test@example.com"},
|
||||||
|
"origin": []string{clientAppDomain},
|
||||||
|
}.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
h.flowPopupPost(rec, req, httprouter.Params{})
|
||||||
|
assert.Equal(t, http.StatusFound, rec.Code)
|
||||||
|
assert.Equal(t, "", rec.Body.String())
|
||||||
|
assert.Equal(t, loginDomain+"/authorize?"+url.Values{
|
||||||
|
"client_id": []string{"test-id"},
|
||||||
|
"login_name": []string{"test@example.com"},
|
||||||
|
"redirect_uri": []string{lavenderDomain + "/callback"},
|
||||||
|
"response_type": []string{"code"},
|
||||||
|
"state": []string{"example.com:" + nextState},
|
||||||
|
}.Encode(), rec.Header().Get("Location"))
|
||||||
|
}
|
||||||
|
@ -5,24 +5,41 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"github.com/1f349/overlapfs"
|
"github.com/1f349/overlapfs"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed *.go.html
|
//go:embed *.go.html
|
||||||
flowPages embed.FS
|
flowPages embed.FS
|
||||||
FlowTemplates *template.Template
|
flowTemplates *template.Template
|
||||||
|
loadOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadPages(wd string) error {
|
func LoadPages(wd string) (err error) {
|
||||||
wwwDir := filepath.Join(wd, "www")
|
loadOnce.Do(func() {
|
||||||
err := os.Mkdir(wwwDir, os.ModePerm)
|
var o fs.FS = flowPages
|
||||||
if err != nil {
|
if wd != "" {
|
||||||
return nil
|
wwwDir := filepath.Join(wd, "www")
|
||||||
}
|
err = os.Mkdir(wwwDir, os.ModePerm)
|
||||||
wdFs := os.DirFS(wwwDir)
|
if err != nil {
|
||||||
o := overlapfs.OverlapFS{A: flowPages, B: wdFs}
|
return
|
||||||
FlowTemplates, err = template.ParseFS(o, "*.go.html")
|
}
|
||||||
|
wdFs := os.DirFS(wwwDir)
|
||||||
|
o = overlapfs.OverlapFS{A: flowPages, B: wdFs}
|
||||||
|
}
|
||||||
|
flowTemplates, err = template.ParseFS(o, "*.go.html")
|
||||||
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RenderPageTemplate(wr io.Writer, name string, data any) {
|
||||||
|
err := flowTemplates.ExecuteTemplate(wr, name+".go.html", data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to render page: %s: %s\n", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user