diff --git a/auth/auth.go b/auth/auth.go index e9f09151b3d1da22557aedfa822e7a00bdbea564..c973b2fc34d0338b08d618eda620b6c0b6e2699c 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -27,6 +27,7 @@ package auth import ( "encoding/json" "net/http" + "strings" "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -61,32 +62,55 @@ func Init(c *Config) (err error) { func newSession() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + request := &NewSessionRequest{} + err := json.NewDecoder(r.Body).Decode(request) + if err != nil { + sendHTTPErrorResponse(w, http.StatusBadRequest, "Error decoding request: "+err.Error()) + return + } + + response := &NewSessionResponse{} + switch strings.ToLower(request.Backend) { + case "oidc": + if auth.oidc == nil { + sendHTTPErrorResponse(w, http.StatusBadRequest, "OIDC authentication is not configured") + return + } + if response.SessionID, err = NewOIDCSession(); err != nil { + sendHTTPErrorResponse(w, http.StatusBadRequest, "Error creating session: "+err.Error()) + return + } + default: + sendHTTPErrorResponse(w, http.StatusBadRequest, "invalid authentication backend: "+request.Backend) + return + } + sendHTTPResponse(w, http.StatusOK, response) }) } func getSession() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, s := getSessionFromBearerToken(r) - if s == nil || s.Username == "" { - sendHTTPResponseInvalidSession(w) + sID := mux.Vars(r)["session-id"] + s := auth.sessions.get(sID) + if s == nil { + sendHTTPErrorResponse(w, http.StatusNotFound, "this session does not exist") return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(s) + sendHTTPResponse(w, http.StatusOK, s) }) } func deleteSession() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sid, s := getSessionFromBearerToken(r) + sID := mux.Vars(r)["session-id"] + s := auth.sessions.get(sID) if s == nil { - sendHTTPResponseInvalidSession(w) + sendHTTPErrorResponse(w, http.StatusNotFound, "this session does not exist") return } - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusOK, HTTPResponse{Message: "you are now logged out"}) + auth.sessions.remove(sID) + sendHTTPResponse(w, http.StatusOK, "you are now logged out") }) } @@ -110,7 +134,7 @@ func listBackends(w http.ResponseWriter, r *http.Request) { } func disabled(w http.ResponseWriter, r *http.Request) { - sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "authentication is disabled"}) + sendHTTPErrorResponse(w, http.StatusBadRequest, "authentication is disabled") } func InstallHTTPHandler(r *mux.Router) { @@ -119,11 +143,14 @@ func InstallHTTPHandler(r *mux.Router) { return } + sessionsHandler := make(handlers.MethodHandler) + sessionsHandler["POST"] = newSession() + r.Handle("/sessions", sessionsHandler) + sessionHandler := make(handlers.MethodHandler) - sessionHandler["POST"] = newSession() sessionHandler["GET"] = getSession() sessionHandler["DELETE"] = deleteSession() - r.Handle("/session", sessionHandler) + r.Handle("/sessions/{session-id}", sessionHandler) r.HandleFunc("/backends", listBackends) if auth.oidc != nil { @@ -142,7 +169,7 @@ func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, s := getSessionFromBearerToken(r) if s == nil || s.Username == "" { - sendHTTPResponseInvalidSession(w) + sendHTTPInvalidSessionResponse(w) return } next.ServeHTTP(w, attachSessionToRequest(r, s)) diff --git a/auth/oidc.go b/auth/oidc.go index 2704037688ce9624a8c97a43339900a1cefffb5a..fab61af719227e4aed836ff2df55f4e062ca7435 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -39,12 +39,17 @@ type OIDCSession struct { token *oauth2.Token } -func NewOIDCSession() (s *OIDCSession, err error) { - s = &OIDCSession{} - if s.State, err = generateRandomString(32); err != nil { +func NewOIDCSession() (sID string, err error) { + os := &OIDCSession{} + if os.State, err = generateRandomString(16); err != nil { return } - if s.Nonce, err = generateRandomString(32); err != nil { + if os.Nonce, err = generateRandomString(16); err != nil { + return + } + + s := &Session{oidc: os} + if sID, err = auth.sessions.insert(s); err != nil { return } return @@ -100,24 +105,13 @@ func (h *oidcLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s != nil { msg := "" if s.Username != "" { - msg = "You are still logged in, please logout first" + msg = "This session is already logged in." } else if s.oidc == nil { msg = "This is not an OIDC session." } else { - msg = "OIDC login already in progress, retry later." + msg = "OIDC login already in progress." } - sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: msg}) - return - } - - os, err := NewOIDCSession() - if err != nil { - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Failed to generate new OIDC session: " + err.Error()}) - return - } - s = &Session{oidc: os, Expires: time.Now().Add(time.Minute)} - if _, err = auth.sessions.insert(s); err != nil { - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Failed to generate new session: " + err.Error()}) + sendHTTPErrorResponse(w, http.StatusConflict, msg) return } http.Redirect(w, r, h.backend.oauth2Config.AuthCodeURL(s.oidc.State, oidc.Nonce(s.oidc.Nonce)), http.StatusFound) @@ -128,61 +122,62 @@ type oidcCallbackHandler struct { } func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - sid, s := getSessionFromBearerToken(r) + // TODO: this won't work, we need to use `state` to find the right session + sID, s := getSessionFromBearerToken(r) if s == nil { - sendHTTPResponseInvalidSession(w) + sendHTTPInvalidSessionResponse(w) return } if s.oidc == nil { - sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: "This is not an OIDC session."}) + sendHTTPErrorResponse(w, http.StatusConflict, "This is not an OIDC session.") return } if s.Username != "" { - sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: "This session is already logged in."}) + sendHTTPErrorResponse(w, http.StatusConflict, "This session is already logged in.") return } if r.URL.Query().Get("state") != s.oidc.State { - sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OIDC verification failed: state did not match"}) + sendHTTPErrorResponse(w, http.StatusBadRequest, "OIDC verification failed: state did not match") return } oauth2Token, err := h.backend.oauth2Config.Exchange(r.Context(), r.URL.Query().Get("code")) if err != nil { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OAuth2 token exchange failed: " + err.Error()}) + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusBadRequest, "OAuth2 token exchange failed: "+err.Error()) return } rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OIDC verification failed: no id_token field in oauth2 token."}) + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusBadRequest, "OIDC verification failed: no id_token field in oauth2 token.") return } // Verify the ID Token signature and nonce. idToken, err := h.backend.verifier.Verify(r.Context(), rawIDToken) if err != nil { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "OAuth2 ID token verification failed: " + err.Error()}) + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusInternalServerError, "OAuth2 ID token verification failed: "+err.Error()) return } if idToken.Nonce != s.oidc.Nonce { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "OAuth2 ID token verification failed: invalid nonce"}) + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusInternalServerError, "OAuth2 ID token verification failed: invalid nonce") return } userInfo, err := h.backend.provider.UserInfo(r.Context(), oauth2.StaticTokenSource(oauth2Token)) if err != nil { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Fetching OIDC UserInfo failed: " + err.Error()}) + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusInternalServerError, "Fetching OIDC UserInfo failed: "+err.Error()) return } newS := &Session{} if err := userInfo.Claims(newS); err != nil { - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Parsing OIDC UserInfo failed: " + err.Error()}) + sendHTTPErrorResponse(w, http.StatusInternalServerError, "Parsing OIDC UserInfo failed: "+err.Error()) return } maxAge := defaultAge @@ -192,10 +187,10 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) newS.Expires = time.Now().Add(maxAge) newS.oidc = &OIDCSession{State: s.oidc.State, Nonce: s.oidc.Nonce} newS.oidc.token = oauth2Token - if err = auth.sessions.update(sid, newS); err != nil { - auth.sessions.remove(sid) - sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Updating session failed: " + err.Error()}) + if err = auth.sessions.update(sID, newS); err != nil { + auth.sessions.remove(sID) + sendHTTPErrorResponse(w, http.StatusInternalServerError, "Updating session failed: "+err.Error()) return } - sendHTTPResponse(w, http.StatusOK, HTTPResponse{Message: "You are now logged in as: " + newS.Username}) + sendHTTPResponse(w, http.StatusOK, "You are now logged in as: "+newS.Username) } diff --git a/auth/sessions.go b/auth/sessions.go index 92f345769d7ca3bb8738aa955a996b8d6bec0c92..7d75333c9332147ca4f8a8a47b62280a1bd31251 100644 --- a/auth/sessions.go +++ b/auth/sessions.go @@ -73,21 +73,21 @@ func getSessionFromBearerToken(r *http.Request) (string, *Session) { return "", nil } - sid, ok := parseBearerAuthHeader(authHeader) + sID, ok := parseBearerAuthHeader(authHeader) if !ok { return "", nil } - s := auth.sessions.get(sid) + s := auth.sessions.get(sID) if s == nil { return "", nil } if s.Expired() { - auth.sessions.remove(sid) + auth.sessions.remove(sID) return "", nil } - return sid, s + return sID, s } func attachSessionToRequest(r *http.Request, s *Session) *http.Request { @@ -125,6 +125,8 @@ func (sm *SessionManager) insert(s *Session) (id string, err error) { return } + // TODO: set session expiry + sm.mutex.Lock() defer sm.mutex.Unlock() sm.sessions[id] = s @@ -146,6 +148,8 @@ func (sm *SessionManager) update(id string, s *Session) error { sm.mutex.Lock() defer sm.mutex.Unlock() + // TODO: set session expiry + if _, ok := sm.sessions[id]; !ok { return errors.New("session not found.") } diff --git a/auth/utils.go b/auth/utils.go index 35b77d1b8f2c2351c9452417134fe0a4b09f98f6..a639f032abb5f3521e86ccc60d8569dbac0bc1a4 100644 --- a/auth/utils.go +++ b/auth/utils.go @@ -45,18 +45,28 @@ func generateRandomString(len int) (string, error) { return base64.RawURLEncoding.EncodeToString(b[:]), nil } -type HTTPResponse struct { - Message string `json:"message,omitempty"` - Error string `json:"error,omitempty"` +type NewSessionRequest struct { + Backend string `json:"backend"` } -func sendHTTPResponse(w http.ResponseWriter, status int, resp HTTPResponse) { +type NewSessionResponse struct { + SessionID string `json:"session-id"` +} + +func sendHTTPResponse(w http.ResponseWriter, status int, resp interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(resp) // TODO: Error Handling? } -func sendHTTPResponseInvalidSession(w http.ResponseWriter) { - resp := HTTPResponse{Error: "Request does not contain a valid token or session is already expired."} - sendHTTPResponse(w, http.StatusUnauthorized, resp) +type HTTPErrorResponse struct { + Error string `json:"error,omitempty"` +} + +func sendHTTPErrorResponse(w http.ResponseWriter, status int, error string) { + sendHTTPResponse(w, status, HTTPErrorResponse{Error: error}) +} + +func sendHTTPInvalidSessionResponse(w http.ResponseWriter) { + sendHTTPErrorResponse(w, http.StatusUnauthorized, "Request does not contain a valid token or session is already expired.") }