From 4a9a913d6617724b325d2e5fbab846657f262e19 Mon Sep 17 00:00:00 2001 From: TQ Hirsch Date: Thu, 19 Oct 2023 07:13:09 +0200 Subject: [PATCH] Initial commit --- app/main.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++ docs/auth-flow.md | 58 +++++++++++++++++++++++++++++++++ go.mod | 3 ++ go.sum | 0 4 files changed, 143 insertions(+) create mode 100644 app/main.go create mode 100644 docs/auth-flow.md create mode 100644 go.mod create mode 100644 go.sum diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..5b0050e --- /dev/null +++ b/app/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + htemplate "html/template" + "log" + "net" + "net/http" + "net/http/fcgi" + "os" + "strings" +) + +var ( + domain = flag.String("domain", "thequux.com", "The base domain to enable SSO for") + listen = flag.String("listen", "0.0.0.0:80", "The address to listen on") +) + +func main() { + flag.Parse() + + l := log.New(os.Stderr, "ipaSSO: ", log.Ldate|log.Ltime|log.Lshortfile) + + mux := http.NewServeMux() + mux.Handle("/env", http.HandlerFunc(dumpEnv)) + l.Printf("Starting") + listener, err := net.Listen("tcp", *listen) + if err != nil { + log.Fatalln("Failed to listen: ", err) + } + l.Println("Listening on", listener.Addr()) + log.Fatal(fcgi.Serve(listener, mux)) +} + +var envTemplate = htemplate.Must(htemplate.New("envdoc").Parse(` + + + + + + +

Headers

+ + {{- range $k,$v := .Headers }} + + + + + {{- end }} +
[{{ $k }}]
{{ $v }}
+

Process Environment

+ + {{- range $k,$v := .ProcessEnv }} + + + + + {{- end }} +
{{ $k }}
{{ $v }}
+ + +`)) + +type envdocArgs struct { + Headers map[string]string + ProcessEnv map[string]string +} + +func dumpEnv(w http.ResponseWriter, r *http.Request) { + var headers = map[string]string{} + for k, v := range r.Header { + headers[k] = strings.Join(v, "\n") + } + var args = envdocArgs{ProcessEnv: fcgi.ProcessEnv(r), Headers: headers} + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + if err := envTemplate.Execute(w, args); err != nil { + panic(err) + } +} diff --git a/docs/auth-flow.md b/docs/auth-flow.md new file mode 100644 index 0000000..daf0bd5 --- /dev/null +++ b/docs/auth-flow.md @@ -0,0 +1,58 @@ +# Authorization flow + +Current authorization state is passwd around via a signed cookie containing authorization details in JSON. + +Most pages do not have any form of authorization, and simply depend on the cookie. +However, in order to log in, more complex flow is necessary. + +Login pages are all grouped under `/login`, which normally serves an HTML form with some additional javascript. +This form includes a hidden field with the page to redirect to. +The JS on `/login` begins by fetching `/login/status` via XHR GET. +`/login/status` returns a JSON response indicating the login state, which is one of: +* VALID: User is fully logged in. User details are attached +* EXPLICIT_LOGOUT: Cookie is set to indicate logged-out state. +* UNKNOWN: No cookie set +* INVALID: Invalid signature on cookie, cookie content invalid, or cookie expired. This also clears the cookie. + +If the state is `UNKNOWN` or `INVALID`, automatic login is performed. +If the state is `EXPLICIT_LOGOUT`, the process terminates here. +Otherwise, a login form is displayed. + +If automatic login fails, the page simply displays a login form. +The user may log in using a username/password, at which point a cookie is set with their details. +The user may also submit the form with blank username/password, in which case automatic login is performed. + +# Automatic login +These steps are performed in sequence, each returning the same data as `/login/status`. +The process completes when the state is `VALID` or `EXPLICIT_LOGOUT`. + +1. Fetch `/login/spnego` via XHR GET. +2. Fetch `/login/x509` via XHR GET. + + Upon successful login, a redirect is performed to the page in the hidden field. + +# API + +In addition to the `/login` endpoint, there exist several other useful endpoints: + +* `/logout`: Set cookie to indicate logged-out state +* `/status`: Fetch a representation of user's info. + If `Accept` requests `application/json`, this is JSON. + Otherwise, this is an HTML page +* `/sigkey`: Fetch the current signing key. This is an Ed25519 key. +* `/refresh`: Refresh the user data cookie given the refresh token. + +# Two cookies + +In the above description, the login token was described as a single cookie. +It is, in fact, two cookies: +* A short-lived (~seconds) user data cookie +* A longer-lived (~8 hour) refresh token, tied to an auth server + +If the user data cookie expires, the refresh token can be fed to `/refresh` to update the user data cookie. +The purpose of the user data cookie is solely to reduce the load on the SSO server; the resources fetched by a page will + generally be fetched within a few seconds of the initial request. +It is the reverse proxy's responsibility to refresh the user data cookie and translate it into headers or FCGI environment variables for the backend server. +This task can be assisted by ipasso-rpxagentd (to be specified). + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..879b235 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.thequux.com/thequux/ipasso + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29