From 416e584ba88bfa760e59f89d491b390eea149c1f Mon Sep 17 00:00:00 2001
From: Christian Pointner <equinox@helsinki.at>
Date: Wed, 27 Mar 2019 01:39:51 +0100
Subject: [PATCH] make repsonses from auth subsystem more consistent

---
 api/v1/utils.go |  2 +-
 auth/auth.go    | 30 ++++++++++++++++++++++--------
 auth/oidc.go    | 38 ++++++++++++++++++--------------------
 auth/utils.go   | 18 ++++++++++++++++++
 4 files changed, 59 insertions(+), 29 deletions(-)

diff --git a/api/v1/utils.go b/api/v1/utils.go
index 39d381c..52f2cc0 100644
--- a/api/v1/utils.go
+++ b/api/v1/utils.go
@@ -110,6 +110,6 @@ func authorizeRequest(w http.ResponseWriter, r *http.Request, showID string) (bo
 			return true, s
 		}
 	}
-	http.Error(w, "you are not allowed to access show: "+showID, http.StatusForbidden)
+	sendWebResponse(w, http.StatusForbidden, ErrorResponse{Error: "you are not allowed to access show: " + showID})
 	return false, s
 }
diff --git a/auth/auth.go b/auth/auth.go
index 8bb10d3..ccc7fd4 100644
--- a/auth/auth.go
+++ b/auth/auth.go
@@ -25,6 +25,7 @@
 package auth
 
 import (
+	"encoding/json"
 	"net/http"
 	"strings"
 
@@ -45,6 +46,7 @@ func Init(c *Config) (err error) {
 		return
 	}
 	auth.config = c
+
 	if auth.sessions, err = NewSessionManager(); err != nil {
 		return
 	}
@@ -54,7 +56,6 @@ func Init(c *Config) (err error) {
 			return
 		}
 	}
-
 	return
 }
 
@@ -63,14 +64,14 @@ func login(w http.ResponseWriter, r *http.Request) {
 	switch strings.ToLower(method) {
 	case "oidc":
 		if auth.oidc == nil {
-			http.Error(w, "OIDC authentication is not configured", http.StatusBadRequest)
+			sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OIDC authentication is not configured"})
 			return
 		}
 		auth.oidc.HandleLogin(w, r)
 	case "":
-		http.Error(w, "default/fallback authentication method has not been implemented yet!", http.StatusNotImplemented)
+		sendHTTPResponse(w, http.StatusNotImplemented, HTTPResponse{Error: "default/fallback authentication method has not been implemented yet"})
 	default:
-		http.Error(w, "invalid authentication method: "+method, http.StatusBadRequest)
+		sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "invalid authentication method: " + method})
 	}
 }
 
@@ -78,15 +79,27 @@ func logout(w http.ResponseWriter, r *http.Request) {
 	sid, s := getSessionFromCookie(r)
 	invalidateSessionCookie(w)
 	if s == nil {
-		http.Error(w, "Request does not contain a valid session cookie or session is already expired.", http.StatusUnauthorized)
+		sendHTTPResponseInvalidSession(w)
 		return
 	}
 	auth.sessions.remove(sid)
-	http.Error(w, "You are now logged out.", http.StatusOK)
+	sendHTTPResponse(w, http.StatusOK, HTTPResponse{Message: "you are now logged out"})
+}
+
+func whoami(w http.ResponseWriter, r *http.Request) {
+	_, s := getSessionFromCookie(r)
+	if s == nil {
+		sendHTTPResponseInvalidSession(w)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	json.NewEncoder(w).Encode(s)
 }
 
 func disabled(w http.ResponseWriter, r *http.Request) {
-	http.Error(w, "authentication is disabled", http.StatusNotAcceptable)
+	sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "authentication is disabled"})
 }
 
 func InstallHTTPHandler(r *mux.Router) {
@@ -97,6 +110,7 @@ func InstallHTTPHandler(r *mux.Router) {
 
 	r.HandleFunc("/login", login)
 	r.HandleFunc("/logout", logout)
+	r.HandleFunc("/whoami", whoami)
 	if auth.oidc != nil {
 		r.Handle("/oidc/callback", auth.oidc.CallbackHandler())
 	}
@@ -112,7 +126,7 @@ func Middleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		_, s := getSessionFromCookie(r)
 		if s == nil {
-			http.Error(w, "Request does not contain a valid session cookie or session is already expired.", http.StatusUnauthorized)
+			sendHTTPResponseInvalidSession(w)
 			return
 		}
 		next.ServeHTTP(w, attachSessionToRequest(r, s))
diff --git a/auth/oidc.go b/auth/oidc.go
index c3c110a..506ed77 100644
--- a/auth/oidc.go
+++ b/auth/oidc.go
@@ -26,7 +26,6 @@ package auth
 
 import (
 	"context"
-	"encoding/json"
 	"net/http"
 	"time"
 
@@ -95,24 +94,24 @@ func (b *OIDCBackend) HandleLogin(w http.ResponseWriter, r *http.Request) {
 	if s != nil {
 		msg := ""
 		if s.Username != "" {
-			msg = "You are still logged in, please logout first."
+			msg = "You are still logged in, please logout first"
 		} else if s.oidc == nil {
 			msg = "This is not an OIDC session."
 		} else {
 			msg = "OIDC login already in progress, retry later."
 		}
-		http.Error(w, msg, http.StatusConflict)
+		sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: msg})
 		return
 	}
 
 	os, err := NewOIDCSession()
 	if err != nil {
-		http.Error(w, "Failed to generate new OIDC session: "+err.Error(), http.StatusInternalServerError)
+		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 sid, err = auth.sessions.insert(s); err != nil {
-		http.Error(w, "Failed to generate new session: "+err.Error(), http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Failed to generate new session: " + err.Error()})
 		return
 	}
 	setSessionCookie(w, sid)
@@ -127,20 +126,20 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	sid, s := getSessionFromCookie(r)
 	if s == nil {
 		invalidateSessionCookie(w)
-		http.Error(w, "Request does not contain a valid session cookie or session is already expired.", http.StatusUnauthorized)
+		sendHTTPResponseInvalidSession(w)
 		return
 	}
 
 	if s.oidc == nil {
-		http.Error(w, "This is not an OIDC session.", http.StatusConflict)
+		sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: "This is not an OIDC session."})
 		return
 	}
 	if s.Username != "" {
-		http.Error(w, "This session is already logged in.", http.StatusConflict)
+		sendHTTPResponse(w, http.StatusConflict, HTTPResponse{Error: "This session is already logged in."})
 		return
 	}
 	if r.URL.Query().Get("state") != s.oidc.State {
-		http.Error(w, "OIDC verification failed: state did not match", http.StatusBadRequest)
+		sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OIDC verification failed: state did not match"})
 		return
 	}
 
@@ -148,14 +147,14 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	if err != nil {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "OAuth2 token exchange failed: "+err.Error(), http.StatusBadRequest)
+		sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OAuth2 token exchange failed: " + err.Error()})
 		return
 	}
 	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 	if !ok {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "OIDC verification failed: no id_token field in oauth2 token.", http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusBadRequest, HTTPResponse{Error: "OIDC verification failed: no id_token field in oauth2 token."})
 		return
 	}
 
@@ -164,13 +163,13 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	if err != nil {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "OAuth2 ID token verification failed: "+err.Error(), http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "OAuth2 ID token verification failed: " + err.Error()})
 		return
 	}
 	if idToken.Nonce != s.oidc.Nonce {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "OAuth2 ID token  verification failed: invalid nonce", http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "OAuth2 ID token verification failed: invalid nonce"})
 		return
 	}
 
@@ -178,30 +177,29 @@ func (h *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	if err != nil {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "Fetching OIDC UserInfo failed: "+err.Error(), http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Fetching OIDC UserInfo failed: " + err.Error()})
 		return
 	}
 
 	newS := &Session{}
 	if err := userInfo.Claims(newS); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Parsing OIDC UserInfo failed: " + err.Error()})
 		return
 	}
 	maxAge := defaultAge
 	if auth.config.Sessions.MaxAge > 0 {
 		maxAge = auth.config.Sessions.MaxAge
 	}
-	newS.Expires = time.Now().Add(maxAge * time.Second)
+	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 {
 		invalidateSessionCookie(w)
 		auth.sessions.remove(sid)
-		http.Error(w, "Updating session failed: "+err.Error(), http.StatusInternalServerError)
+		sendHTTPResponse(w, http.StatusInternalServerError, HTTPResponse{Error: "Updating session failed: " + err.Error()})
 		return
 	}
-	setSessionCookie(w, sid) // reset the cookie to update max-age
+	setSessionCookie(w, sid) // re-set the cookie to update max-age
 
-	data, _ := json.MarshalIndent(newS, "", "    ")
-	w.Write(data)
+	sendHTTPResponse(w, http.StatusOK, HTTPResponse{Message: "You are now logged in as: " + newS.Username})
 }
diff --git a/auth/utils.go b/auth/utils.go
index c780768..39c8fcc 100644
--- a/auth/utils.go
+++ b/auth/utils.go
@@ -27,6 +27,8 @@ package auth
 import (
 	"crypto/rand"
 	"encoding/base64"
+	"encoding/json"
+	"net/http"
 )
 
 type contextKey int
@@ -42,3 +44,19 @@ 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"`
+}
+
+func sendHTTPResponse(w http.ResponseWriter, status int, resp HTTPResponse) {
+	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 session cookie or session is already expired."}
+	sendHTTPResponse(w, http.StatusUnauthorized, resp)
+}
-- 
GitLab