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
... | ... |
@@ -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 |
+} |