loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit 15423135b3c55ecbe4ca31d4247e4b022e313ecc
parent dfc3994b375e5e0a316a4d667d62813aa42f2ff4
Author: Sebastian Mancke <sebastian.mancke@snabble.io>
Date:   Thu, 27 Dec 2018 22:01:07 +0100

Merge branch 'perenecabuto-master'

Diffstat:
MREADME.md | 8+++++++-
Mlogin/handler.go | 34++++++++++++++++++++++++++++------
Mlogin/handler_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 82 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md @@ -100,11 +100,17 @@ $ docker run -d -p 8080:8080 -e LOGINSRV_JWT_SECRET=my_secret -e LOGINSRV_BACKEN ### GET /login -Returns a simple bootstrap styled login form. +Per default, it returns a simple bootstrap styled login form for unauthenticated requests an a small page with user info authenticated requests. +When the call accepts a JSON output, the json content of the token is returned authenticated requests. The returned HTML follows the UI composition conventions from (lib-compose)[https://github.com/tarent/lib-compose], so it can be embedded into an existing layout. +| Parameter-Type | Parameter | Description | | +| ------------------|--------------------------------------------------|-------------------------------------------------------------------|--------------| +| Http-Header | Accept: text/html | Return the login form or user html. | default | +| Http-Header | Accept: application/json | Return the user Object as json, or 403 if not authenticated. | | + ### GET /login/<provider> Starts the OAuth Web Flow with the configured provider. E.g. `GET /login/github` redirects to the GitHub login form. diff --git a/login/handler.go b/login/handler.go @@ -17,6 +17,7 @@ import ( const contentTypeHTML = "text/html; charset=utf-8" const contentTypeJWT = "application/jwt" +const contentTypeJSON = "application/json" const contentTypePlain = "text/plain" type userClaimsFunc func(userInfo model.UserInfo) (jwt.Claims, error) @@ -122,7 +123,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") if !(r.Method == "GET" || r.Method == "DELETE" || (r.Method == "POST" && - (strings.HasPrefix(contentType, "application/json") || + (strings.HasPrefix(contentType, contentTypeJSON) || strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || strings.HasPrefix(contentType, "multipart/form-data") || contentType == ""))) { @@ -147,6 +148,16 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { userInfo, valid := h.GetToken(r) + if wantJSON(r) { + if valid { + w.Header().Set("Content-Type", contentTypeJSON) + enc := json.NewEncoder(w) + enc.Encode(userInfo) // ignore error of encoding + } else { + h.respondAuthFailure(w, r) + } + return + } writeLoginForm(w, loginFormData{ Config: h.config, @@ -176,7 +187,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { h.respondAuthFailure(w, r) return } - + h.respondBadRequest(w, r) return } @@ -375,17 +386,28 @@ func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", contentTypePlain) - w.WriteHeader(403) - fmt.Fprintf(w, "Wrong credentials") + if wantJSON(r) { + w.Header().Set("Content-Type", contentTypeJSON) + w.WriteHeader(403) + fmt.Fprintf(w, `{"error": "Wrong credentials"}`) + } else { + w.Header().Set("Content-Type", contentTypePlain) + w.WriteHeader(403) + fmt.Fprintf(w, "Wrong credentials") + } + } func wantHTML(r *http.Request) bool { return strings.Contains(r.Header.Get("Accept"), "text/html") } +func wantJSON(r *http.Request) bool { + return strings.Contains(r.Header.Get("Accept"), contentTypeJSON) +} + func getCredentials(r *http.Request) (string, string, error) { - if r.Header.Get("Content-Type") == "application/json" { + if r.Header.Get("Content-Type") == contentTypeJSON { m := map[string]string{} body, err := ioutil.ReadAll(r.Body) if err != nil { diff --git a/login/handler_test.go b/login/handler_test.go @@ -1,10 +1,12 @@ package login import ( + "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" + "net/url" "strconv" "strings" "testing" @@ -414,6 +416,51 @@ func TestHandler_getToken_Valid(t *testing.T) { Equal(t, input, userInfo) } +func TestHandler_ReturnUserInfoJSON(t *testing.T) { + h := testHandler() + input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} + token, err := h.createToken(input) + NoError(t, err) + url, _ := url.Parse("/context/login") + r := &http.Request{ + Method: "GET", + URL: url, + Header: http.Header{ + "Cookie": {h.config.CookieName + "=" + token + ";"}, + "Accept": {"application/json"}, + }, + } + + recorder := call(r) + Equal(t, 200, recorder.Code) + Equal(t, "application/json", recorder.Header().Get("Content-Type")) + + output := model.UserInfo{} + json.Unmarshal(recorder.Body.Bytes(), &output) + + Equal(t, input, output) +} + +func TestHandler_ReturnUserInfoJSON_InvalidToken(t *testing.T) { + h := testHandler() + url, _ := url.Parse("/context/login") + r := &http.Request{ + Method: "GET", + URL: url, + Header: http.Header{ + "Cookie": {h.config.CookieName + "= 123;"}, + "Accept": {"application/json"}, + }, + } + + recorder := call(r) + Equal(t, 403, recorder.Code) + Equal(t, "application/json", recorder.Header().Get("Content-Type")) + output := map[string]interface{}{} + json.Unmarshal(recorder.Body.Bytes(), &output) + Equal(t, map[string]interface{}{"error": "Wrong credentials"}, output) +} + func TestHandler_signAndVerify_ES256(t *testing.T) { h := testHandler() h.config.JwtAlgo = "ES256"