diff --git a/app/ipasso/cookies.go b/app/ipasso/cookies.go new file mode 100644 index 0000000..12f7b6f --- /dev/null +++ b/app/ipasso/cookies.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "git.thequux.com/thequux/ipasso/backend" + "go.uber.org/zap" + "net/http" +) + +type loginStateMw struct { + handler http.Handler +} + +type Session struct { + State LoginState + Session *backend.Session + Cache *backend.SessionCache +} + +func (l loginStateMw) ServeHTTP(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + var cookie *http.Cookie + var session Session + session.State = LS_Unknown + for _, cookie = range req.Cookies() { + // TODO: make host configurable + if cookie.Name != "IPASSO_SID" { + continue + } + + sid := cookie.Value + if sid == "[logout]" { + session.State = LS_LoggedOut + break + } + bsession, bcache, err := backend.SessionStore.GetSession(ctx, sid) + if err != nil { + zap.L().Warn("Failed to fetch cookie", zap.Error(err)) + // cookie was invalid + session.State = LS_Invalid + } else { + session.State = LS_Valid + session.Session = &bsession + session.Cache = bcache + } + break + } + ctx = context.WithValue(ctx, "ipa.Session", &session) + l.handler.ServeHTTP(w, req.WithContext(ctx)) +} + +func GetSession(ctx context.Context, loadCache bool) *Session { + session := ctx.Value("ipa.Session").(*Session) + if session != nil && session.Session != nil && loadCache && (session.Cache == nil || !session.Cache.Valid) { + cache, err := buildSessionCache(session.Session, nil) + if err != nil { + zap.L().Warn("Failed to build session cache", zap.Error(err)) + } else { + session.Cache = &cache + } + } else { + zap.L().Debug("Not rebuilding session cache", zap.Any("session", session)) + } + return session +} + +func LoginStateMw(handler http.Handler) http.Handler { + return loginStateMw{handler} +} diff --git a/app/ipasso/ldap.go b/app/ipasso/ldap.go index 4537d69..1e410a1 100644 --- a/app/ipasso/ldap.go +++ b/app/ipasso/ldap.go @@ -205,7 +205,7 @@ func buildSessionCache(b *backend.Session, entry *ldap.Entry) (cache backend.Ses } else if attr.Name == "givenName" && len(attr.Values) > 0 { cache.GivenName = strings.Join(attr.Values, " ") } else if attr.Name == "sn" { - cache.SurName = strings.Join(attr.Values, " ") + cache.FamilyName = strings.Join(attr.Values, " ") } else if attr.Name == "mail" && len(attr.Values) > 0 { cache.Email = attr.Values[0] } else if attr.Name == "memberOf" { diff --git a/app/ipasso/loginKrb.go b/app/ipasso/loginKrb.go index 3f2cb41..1e574f5 100644 --- a/app/ipasso/loginKrb.go +++ b/app/ipasso/loginKrb.go @@ -111,4 +111,18 @@ func finishLogin(w http.ResponseWriter, req *http.Request, entry *ldap.Entry, ex _ = backend.SessionStore.PutSession(req.Context(), session, sessionCache) // TODO: set cookies, return success + + cookie := http.Cookie{ + Name: "IPASSO_SID", + Value: SessionID, + Domain: req.Host, + HttpOnly: true, + Secure: true, + Expires: session.Expiration, + SameSite: http.SameSiteStrictMode, + } + + //http.SetCookie(w, &cookie) + cookie.Domain = "." + *domain + http.SetCookie(w, &cookie) } diff --git a/app/ipasso/loginState.go b/app/ipasso/loginState.go new file mode 100644 index 0000000..4e4640a --- /dev/null +++ b/app/ipasso/loginState.go @@ -0,0 +1,63 @@ +package main + +import ( + "git.thequux.com/thequux/ipasso/util/startup" + "github.com/julienschmidt/httprouter" + "net/http" + "time" +) + +func init() { + startup.Routes.Add(func(router *httprouter.Router) { + router.Handler("GET", "/login/info", LoginStateMw(http.HandlerFunc(stateServlet))) + }) +} + +type LoginState string + +var ( + LS_Valid LoginState = "VALID" + LS_LoggedOut LoginState = "EXPLICIT_LOGOUT" + LS_Unknown LoginState = "UNKNOWN" + LS_Invalid LoginState = "INVALID" +) + +type PublicLoginState struct { + State LoginState `json:"login_state"` + Expiration *time.Time `json:"expiration,omitempty"` + UserId string `json:"uid,omitempty"` + LdapDn string `json:"ldap_dn,omitempty"` + + Groups []string `json:"groups,omitempty"` + DisplayName string `json:"display_name,omitempty"` + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + Email string `json:"email,omitempty"` +} + +func GetPublicState(s *Session) PublicLoginState { + ret := PublicLoginState{ + State: s.State, + } + if s.Session != nil { + ret.Expiration = &s.Session.Expiration + ret.UserId = s.Session.UserID + ret.LdapDn = s.Session.LdapDN + } + + if s.Cache != nil { + ret.Groups = s.Cache.Groups + ret.DisplayName = s.Cache.DisplayName + ret.GivenName = s.Cache.GivenName + ret.FamilyName = s.Cache.FamilyName + ret.Email = s.Cache.Email + } + + return ret +} + +func stateServlet(w http.ResponseWriter, req *http.Request) { + session := GetSession(req.Context(), true) + data := GetPublicState(session) + RenderPage(w, req, "status", data) +} diff --git a/backend/interface.go b/backend/interface.go index 9cb6098..2022a7f 100644 --- a/backend/interface.go +++ b/backend/interface.go @@ -35,7 +35,7 @@ type SessionCache struct { Groups []string DisplayName string GivenName string - SurName string + FamilyName string Email string } diff --git a/backend/redis/redis.go b/backend/redis/redis.go index 5b2cc2b..6de1de1 100644 --- a/backend/redis/redis.go +++ b/backend/redis/redis.go @@ -3,6 +3,7 @@ package redis import ( "context" "encoding/json" + "fmt" "git.thequux.com/thequux/ipasso/backend" "github.com/redis/go-redis/v9" "net/url" @@ -59,12 +60,12 @@ type RedisBackend struct { var putSessionScript = redis.NewScript(` local skey = KEYS[1] local ckey = KEYS[2] -local session = ARGS[1] -local sexp = ARGS[2] -local cache = ARGS[3] -local clifetime = ARGS[4] +local session = ARGV[1] +local sexp = ARGV[2] +local cache = ARGV[3] +local clifetime = ARGV[4] -if redis.call("GET", skey) != "" then +if redis.call("GET", skey) ~= "" then return false else redis.call("SET", skey, session, "EXAT", sexp) @@ -103,29 +104,32 @@ func (r *RedisBackend) PutSession(ctx context.Context, session backend.Session, func (r *RedisBackend) GetSession(ctx context.Context, id string) (backend.Session, *backend.SessionCache, error) { var session backend.Session var cache backend.SessionCache + var cachep = &cache result, err := r.rdb.MGet(ctx, sessionKey(id), scacheKey(id)).Result() if err != nil { return session, nil, err } - v, ok := result[0].([]byte) + fmt.Printf("Result: %#v\n", result) + v, ok := result[0].(string) if !ok { return backend.Session{}, nil, backend.ErrBackendData } - if err = json.Unmarshal(v, &session); err != nil { + if err = json.Unmarshal([]byte(v), &session); err != nil { return backend.Session{}, nil, err } - v, ok = result[1].([]byte) - if !ok { - return backend.Session{}, nil, backend.ErrBackendData - } - if err = json.Unmarshal(v, &cache); err != nil { - return backend.Session{}, nil, err + v, ok = result[1].(string) + if ok { + if err = json.Unmarshal([]byte(v), &cache); err != nil { + return backend.Session{}, nil, err + } + } else { + cachep = nil } - return session, &cache, nil + return session, cachep, nil } func (r *RedisBackend) EndSession(ctx context.Context, id string) { diff --git a/docs/auth-flow.md b/docs/auth-flow.md index daf0bd5..d0c9ee9 100644 --- a/docs/auth-flow.md +++ b/docs/auth-flow.md @@ -1,6 +1,7 @@ # Authorization flow -Current authorization state is passwd around via a signed cookie containing authorization details in JSON. +Current authorization state is passed around via an opaque session ID in a cookie. +This cookie is stored in duplicate: once on the SSO domain and once on the protected domain (for use by RPX's) 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. @@ -26,7 +27,7 @@ The user may also submit the form with blank username/password, in which case au 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. +1. Fetch `/login/krb5` via XHR GET. 2. Fetch `/login/x509` via XHR GET. Upon successful login, a redirect is performed to the page in the hidden field. @@ -36,23 +37,8 @@ The process completes when the state is `VALID` or `EXPLICIT_LOGOUT`. 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. +* `/login/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). - +An OAuth interface is also available on **TODO** \ No newline at end of file diff --git a/resources/templates/login/status.html b/resources/templates/login/status.html new file mode 100644 index 0000000..fa9aace --- /dev/null +++ b/resources/templates/login/status.html @@ -0,0 +1,10 @@ + + +
+ +