Successfully fetch login status
This commit is contained in:
69
app/ipasso/cookies.go
Normal file
69
app/ipasso/cookies.go
Normal file
@@ -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}
|
||||
}
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
63
app/ipasso/loginState.go
Normal file
63
app/ipasso/loginState.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -35,7 +35,7 @@ type SessionCache struct {
|
||||
Groups []string
|
||||
DisplayName string
|
||||
GivenName string
|
||||
SurName string
|
||||
FamilyName string
|
||||
Email string
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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**
|
||||
10
resources/templates/login/status.html
Normal file
10
resources/templates/login/status.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login status</title>
|
||||
</head>
|
||||
<body>
|
||||
{{-
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user