Browse code

Wire login handler

Fix login redirects, tests

Render login and implicit forms as html

Redirect on successful login

Redirect auth needed to login flow

Update session with user info on successful login

Jordan Liggitt authored on 2014/10/16 03:39:19
Showing 8 changed files
... ...
@@ -3,12 +3,13 @@ package login
3 3
 import (
4 4
 	"net/http"
5 5
 	"net/url"
6
+	"strings"
6 7
 )
7 8
 
8 9
 func failed(reason string, w http.ResponseWriter, req *http.Request) {
9 10
 	uri, err := getBaseURL(req)
10 11
 	if err != nil {
11
-		http.Redirect(w, req, req.URL.Path, http.StatusTemporaryRedirect)
12
+		http.Redirect(w, req, req.URL.Path, http.StatusFound)
12 13
 		return
13 14
 	}
14 15
 	query := url.Values{}
... ...
@@ -17,7 +18,7 @@ func failed(reason string, w http.ResponseWriter, req *http.Request) {
17 17
 		query.Set("then", req.URL.Query().Get("then"))
18 18
 	}
19 19
 	uri.RawQuery = query.Encode()
20
-	http.Redirect(w, req, uri.String(), http.StatusTemporaryRedirect)
20
+	http.Redirect(w, req, uri.String(), http.StatusFound)
21 21
 }
22 22
 
23 23
 func getBaseURL(req *http.Request) (*url.URL, error) {
... ...
@@ -28,3 +29,22 @@ func getBaseURL(req *http.Request) (*url.URL, error) {
28 28
 	uri.Scheme, uri.Host, uri.RawQuery, uri.Fragment = req.URL.Scheme, req.URL.Host, "", ""
29 29
 	return uri, nil
30 30
 }
31
+
32
+func postForm(url string, body url.Values) (resp *http.Response, err error) {
33
+	tr := &http.Transport{}
34
+	req, err := http.NewRequest("POST", url, strings.NewReader(body.Encode()))
35
+	if err != nil {
36
+		return nil, err
37
+	}
38
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
39
+	return tr.RoundTrip(req)
40
+}
41
+
42
+func getUrl(url string) (resp *http.Response, err error) {
43
+	tr := &http.Transport{}
44
+	req, err := http.NewRequest("GET", url, nil)
45
+	if err != nil {
46
+		return nil, err
47
+	}
48
+	return tr.RoundTrip(req)
49
+}
... ...
@@ -123,6 +123,8 @@ var DefaultConfirmFormRenderer = confirmTemplateRenderer{}
123 123
 type confirmTemplateRenderer struct{}
124 124
 
125 125
 func (r confirmTemplateRenderer) Render(form ConfirmForm, w http.ResponseWriter, req *http.Request) {
126
+	w.Header().Add("Content-Type", "text/html")
127
+	w.WriteHeader(http.StatusOK)
126 128
 	if err := confirmTemplate.Execute(w, form); err != nil {
127 129
 		glog.Errorf("Unable render confirm template: %v", err)
128 130
 	}
... ...
@@ -93,20 +93,21 @@ func TestImplicit(t *testing.T) {
93 93
 
94 94
 		var resp *http.Response
95 95
 		if testCase.PostValues != nil {
96
-			r, err := http.PostForm(server.URL+testCase.Path, testCase.PostValues)
96
+			r, err := postForm(server.URL+testCase.Path, testCase.PostValues)
97 97
 			if err != nil {
98 98
 				t.Errorf("%s: unexpected error: %v", k, err)
99 99
 				continue
100 100
 			}
101 101
 			resp = r
102 102
 		} else {
103
-			r, err := http.Get(server.URL + testCase.Path)
103
+			r, err := getUrl(server.URL + testCase.Path)
104 104
 			if err != nil {
105 105
 				t.Errorf("%s: unexpected error: %v", k, err)
106 106
 				continue
107 107
 			}
108 108
 			resp = r
109 109
 		}
110
+		defer resp.Body.Close()
110 111
 
111 112
 		if testCase.ExpectStatusCode != 0 && testCase.ExpectStatusCode != resp.StatusCode {
112 113
 			t.Errorf("%s: unexpected response: %#v", k, resp)
... ...
@@ -1,6 +1,12 @@
1 1
 package login
2 2
 
3
-import ()
3
+import "net/http"
4
+
5
+// mux is an object that can register http handlers.
6
+type Mux interface {
7
+	Handle(pattern string, handler http.Handler)
8
+	HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
9
+}
4 10
 
5 11
 type CSRF interface {
6 12
 	Generate() (string, error)
... ...
@@ -3,6 +3,7 @@ package login
3 3
 import (
4 4
 	"html/template"
5 5
 	"net/http"
6
+	"strings"
6 7
 
7 8
 	"github.com/golang/glog"
8 9
 
... ...
@@ -46,6 +47,15 @@ func NewLogin(csrf CSRF, auth PasswordAuthenticator, render LoginFormRenderer) *
46 46
 	}
47 47
 }
48 48
 
49
+// Install registers the login handler into a mux. It is expected that the
50
+// provided prefix will serve all operations. Path MUST NOT end in a slash.
51
+func (l *Login) Install(mux Mux, paths ...string) {
52
+	for _, path := range paths {
53
+		path = strings.TrimRight(path, "/")
54
+		mux.HandleFunc(path, l.ServeHTTP)
55
+	}
56
+}
57
+
49 58
 func (l *Login) ServeHTTP(w http.ResponseWriter, req *http.Request) {
50 59
 	switch req.Method {
51 60
 	case "GET":
... ...
@@ -118,6 +128,8 @@ var DefaultLoginFormRenderer = loginTemplateRenderer{}
118 118
 type loginTemplateRenderer struct{}
119 119
 
120 120
 func (r loginTemplateRenderer) Render(form LoginForm, w http.ResponseWriter, req *http.Request) {
121
+	w.Header().Add("Content-Type", "text/html")
122
+	w.WriteHeader(http.StatusOK)
121 123
 	if err := loginTemplate.Execute(w, form); err != nil {
122 124
 		glog.Errorf("Unable to render login template: %v", err)
123 125
 	}
... ...
@@ -130,6 +142,6 @@ var loginTemplate = template.Must(template.New("loginForm").Parse(`
130 130
   <input type="hidden" name="csrf" value="{{ .Values.CSRF }}">
131 131
   <label>Login: <input type="text" name="username" value="{{ .Values.Username }}"></label>
132 132
   <label>Password: <input type="password" name="password" value=""></label>
133
-  <input type="submit" name="Login">
133
+  <input type="submit" value="Login">
134 134
 </form>
135 135
 `))
... ...
@@ -142,20 +142,21 @@ func TestLogin(t *testing.T) {
142 142
 
143 143
 		var resp *http.Response
144 144
 		if testCase.PostValues != nil {
145
-			r, err := http.PostForm(server.URL+testCase.Path, testCase.PostValues)
145
+			r, err := postForm(server.URL+testCase.Path, testCase.PostValues)
146 146
 			if err != nil {
147 147
 				t.Errorf("%s: unexpected error: %v", k, err)
148 148
 				continue
149 149
 			}
150 150
 			resp = r
151 151
 		} else {
152
-			r, err := http.Get(server.URL + testCase.Path)
152
+			r, err := getUrl(server.URL + testCase.Path)
153 153
 			if err != nil {
154 154
 				t.Errorf("%s: unexpected error: %v", k, err)
155 155
 				continue
156 156
 			}
157 157
 			resp = r
158 158
 		}
159
+		defer resp.Body.Close()
159 160
 
160 161
 		if testCase.ExpectStatusCode != 0 && testCase.ExpectStatusCode != resp.StatusCode {
161 162
 			t.Errorf("%s: unexpected response: %#v", k, resp)
... ...
@@ -42,3 +42,13 @@ func (a *SessionAuthenticator) AuthenticateRequest(req *http.Request) (api.UserI
42 42
 		Name: name,
43 43
 	}, true, nil
44 44
 }
45
+
46
+func (a *SessionAuthenticator) AuthenticationSucceeded(user api.UserInfo, w http.ResponseWriter, req *http.Request) error {
47
+	session, err := a.store.Get(req, a.name)
48
+	if err != nil {
49
+		return err
50
+	}
51
+	values := session.Values()
52
+	values[UserNameKey] = user.GetName()
53
+	return a.store.Save(w, req)
54
+}
... ...
@@ -3,12 +3,15 @@ package origin
3 3
 import (
4 4
 	"fmt"
5 5
 	"net/http"
6
+	"net/url"
6 7
 
7 8
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
8 9
 
9 10
 	"github.com/openshift/origin/pkg/auth/api"
11
+	"github.com/openshift/origin/pkg/auth/authenticator"
10 12
 	"github.com/openshift/origin/pkg/auth/oauth/handlers"
11 13
 	"github.com/openshift/origin/pkg/auth/oauth/registry"
14
+	"github.com/openshift/origin/pkg/auth/server/login"
12 15
 	"github.com/openshift/origin/pkg/auth/server/session"
13 16
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
14 17
 	oauthetcd "github.com/openshift/origin/pkg/oauth/registry/etcd"
... ...
@@ -18,6 +21,7 @@ import (
18 18
 
19 19
 const (
20 20
 	OpenShiftOAuthAPIPrefix = "/oauth"
21
+	OpenShiftLoginPrefix    = "/login"
21 22
 )
22 23
 
23 24
 type AuthConfig struct {
... ...
@@ -41,7 +45,7 @@ func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string {
41 41
 		storage,
42 42
 		osinserver.AuthorizeHandlers{
43 43
 			handlers.NewAuthorizeAuthenticator(
44
-				emptyAuth{},
44
+				&redirectAuthHandler{RedirectURL: OpenShiftLoginPrefix, ThenParam: "then"},
45 45
 				sessionAuth,
46 46
 			),
47 47
 			handlers.NewGrantCheck(
... ...
@@ -55,8 +59,12 @@ func (c *AuthConfig) InstallAPI(mux cmdutil.Mux) []string {
55 55
 	)
56 56
 	server.Install(mux, OpenShiftOAuthAPIPrefix)
57 57
 
58
+	login := login.NewLogin(emptyCsrf{}, &sessionPasswordAuthenticator{emptyPasswordAuth{}, sessionAuth}, login.DefaultLoginFormRenderer)
59
+	login.Install(mux, OpenShiftLoginPrefix)
60
+
58 61
 	return []string{
59 62
 		fmt.Sprintf("Started OAuth2 API at %%s%s", OpenShiftOAuthAPIPrefix),
63
+		fmt.Sprintf("Started login server at %%s%s", OpenShiftLoginPrefix),
60 64
 	}
61 65
 }
62 66
 
... ...
@@ -69,6 +77,32 @@ func (emptyAuth) AuthenticationError(err error, w http.ResponseWriter, req *http
69 69
 	fmt.Fprintf(w, "<body>AuthenticationError - %s</body>", err)
70 70
 }
71 71
 
72
+// Captures the original request url as a "then" param in a redirect to a login flow
73
+type redirectAuthHandler struct {
74
+	RedirectURL string
75
+	ThenParam   string
76
+}
77
+
78
+func (auth *redirectAuthHandler) AuthenticationNeeded(w http.ResponseWriter, req *http.Request) {
79
+	redirectURL, err := url.Parse(auth.RedirectURL)
80
+	if err != nil {
81
+		auth.AuthenticationError(err, w, req)
82
+		return
83
+	}
84
+	if len(auth.ThenParam) != 0 {
85
+		redirectURL.RawQuery = url.Values{
86
+			auth.ThenParam: {req.URL.String()},
87
+		}.Encode()
88
+	}
89
+	http.Redirect(w, req, redirectURL.String(), http.StatusFound)
90
+}
91
+
92
+func (auth *redirectAuthHandler) AuthenticationError(err error, w http.ResponseWriter, req *http.Request) {
93
+	w.Header().Add("Content-Type", "text/html")
94
+	w.WriteHeader(http.StatusForbidden)
95
+	fmt.Fprintf(w, "<body>AuthenticationError - %s</body>", err)
96
+}
97
+
72 98
 type emptyGrant struct{}
73 99
 
74 100
 func (emptyGrant) GrantNeeded(grant *api.Grant, w http.ResponseWriter, req *http.Request) {
... ...
@@ -78,3 +112,55 @@ func (emptyGrant) GrantNeeded(grant *api.Grant, w http.ResponseWriter, req *http
78 78
 func (emptyGrant) GrantError(err error, w http.ResponseWriter, req *http.Request) {
79 79
 	fmt.Fprintf(w, "<body>GrantError - %s</body>", err)
80 80
 }
81
+
82
+type emptyCsrf struct{}
83
+
84
+func (emptyCsrf) Generate() (string, error) {
85
+	return "", nil
86
+}
87
+
88
+func (emptyCsrf) Check(string) (bool, error) {
89
+	return true, nil
90
+}
91
+
92
+//
93
+// Approves any login attempt with non-blank username and password
94
+//
95
+type emptyPasswordAuth struct{}
96
+
97
+func (emptyPasswordAuth) AuthenticatePassword(user, password string) (api.UserInfo, bool, error) {
98
+	if user == "" || password == "" {
99
+		return nil, false, nil
100
+	}
101
+	return &api.DefaultUserInfo{
102
+		Name: user,
103
+	}, true, nil
104
+}
105
+
106
+//
107
+// Saves the username of any successful password authentication in the session
108
+//
109
+type sessionPasswordAuthenticator struct {
110
+	passwordAuthenticator authenticator.Password
111
+	sessionAuthenticator  *session.SessionAuthenticator
112
+}
113
+
114
+// for login.PasswordAuthenticator
115
+func (auth *sessionPasswordAuthenticator) AuthenticatePassword(user, password string) (api.UserInfo, bool, error) {
116
+	return auth.passwordAuthenticator.AuthenticatePassword(user, password)
117
+}
118
+
119
+// for login.PasswordAuthenticator
120
+func (auth *sessionPasswordAuthenticator) AuthenticationSucceeded(user api.UserInfo, then string, w http.ResponseWriter, req *http.Request) {
121
+	err := auth.sessionAuthenticator.AuthenticationSucceeded(user, w, req)
122
+	if err != nil {
123
+		fmt.Fprintf(w, "<body>Could not save session, err=%#v</body>", err)
124
+		return
125
+	}
126
+
127
+	if len(then) != 0 {
128
+		http.Redirect(w, req, then, http.StatusFound)
129
+	} else {
130
+		fmt.Fprintf(w, "<body>PasswordAuthenticationSucceeded - user=%#v</body>", user)
131
+	}
132
+}