CSRF updates - 1.2.0
Clayton Coleman authored on 2016/05/25 11:01:54... | ... |
@@ -8,13 +8,22 @@ |
8 | 8 |
* Controller of the openshiftConsole |
9 | 9 |
*/ |
10 | 10 |
angular.module('openshiftConsole') |
11 |
- .controller('OAuthController', function ($location, $q, RedirectLoginService, DataService, AuthService, Logger) { |
|
11 |
+ .controller('OAuthController', function ($scope, $location, $q, RedirectLoginService, DataService, AuthService, Logger) { |
|
12 | 12 |
var authLogger = Logger.get("auth"); |
13 | 13 |
|
14 |
+ // Initialize to a no-op function. |
|
15 |
+ // Needed to let the view confirm a login when the state is unverified. |
|
16 |
+ $scope.completeLogin = function(){}; |
|
17 |
+ $scope.cancelLogin = function() { |
|
18 |
+ $location.replace(); |
|
19 |
+ $location.url("./"); |
|
20 |
+ }; |
|
21 |
+ |
|
14 | 22 |
RedirectLoginService.finish() |
15 | 23 |
.then(function(data) { |
16 | 24 |
var token = data.token; |
17 | 25 |
var then = data.then; |
26 |
+ var verified = data.verified; |
|
18 | 27 |
var ttl = data.ttl; |
19 | 28 |
|
20 | 29 |
// Try to fetch the user |
... | ... |
@@ -25,21 +34,41 @@ angular.module('openshiftConsole') |
25 | 25 |
.then(function(user) { |
26 | 26 |
// Set the new user and token in the auth service |
27 | 27 |
authLogger.log("OAuthController, got user", user); |
28 |
- AuthService.setUser(user, token, ttl); |
|
29 | 28 |
|
30 |
- // Redirect to original destination (or default to '/') |
|
31 |
- var destination = then || './'; |
|
32 |
- if (URI(destination).is('absolute')) { |
|
33 |
- authLogger.log("OAuthController, invalid absolute redirect", destination); |
|
34 |
- destination = './'; |
|
29 |
+ $scope.completeLogin = function() { |
|
30 |
+ // Persist the user |
|
31 |
+ AuthService.setUser(user, token, ttl); |
|
32 |
+ |
|
33 |
+ // Redirect to original destination (or default to './') |
|
34 |
+ var destination = then || './'; |
|
35 |
+ if (URI(destination).is('absolute')) { |
|
36 |
+ authLogger.log("OAuthController, invalid absolute redirect", destination); |
|
37 |
+ destination = './'; |
|
38 |
+ } |
|
39 |
+ authLogger.log("OAuthController, redirecting", destination); |
|
40 |
+ $location.replace(); |
|
41 |
+ $location.url(destination); |
|
42 |
+ }; |
|
43 |
+ |
|
44 |
+ if (verified) { |
|
45 |
+ // Automatically complete |
|
46 |
+ $scope.completeLogin(); |
|
47 |
+ } else { |
|
48 |
+ // Require the UI to prompt |
|
49 |
+ $scope.confirmUser = user; |
|
50 |
+ |
|
51 |
+ // Additionally, give the UI info about the user being overridden |
|
52 |
+ var currentUser = AuthService.UserStore().getUser(); |
|
53 |
+ if (currentUser && currentUser.metadata.name !== user.metadata.name) { |
|
54 |
+ $scope.overriddenUser = currentUser; |
|
55 |
+ } |
|
35 | 56 |
} |
36 |
- authLogger.log("OAuthController, redirecting", destination); |
|
37 |
- $location.url(destination); |
|
38 | 57 |
}) |
39 | 58 |
.catch(function(rejection) { |
40 | 59 |
// Handle an API error response fetching the user |
41 | 60 |
var redirect = URI('error').query({error: 'user_fetch_failed'}).toString(); |
42 | 61 |
authLogger.error("OAuthController, error fetching user", rejection, "redirecting", redirect); |
62 |
+ $location.replace(); |
|
43 | 63 |
$location.url(redirect); |
44 | 64 |
}); |
45 | 65 |
|
... | ... |
@@ -51,6 +80,7 @@ angular.module('openshiftConsole') |
51 | 51 |
error_uri: rejection.error_uri || "" |
52 | 52 |
}).toString(); |
53 | 53 |
authLogger.error("OAuthController, error", rejection, "redirecting", redirect); |
54 |
+ $location.replace(); |
|
54 | 55 |
$location.url(redirect); |
55 | 56 |
}); |
56 | 57 |
|
... | ... |
@@ -29,6 +29,70 @@ angular.module('openshiftConsole') |
29 | 29 |
this.$get = function($location, $q, Logger) { |
30 | 30 |
var authLogger = Logger.get("auth"); |
31 | 31 |
|
32 |
+ var getRandomInts = function(length) { |
|
33 |
+ var randomValues; |
|
34 |
+ |
|
35 |
+ if (window.crypto && window.Uint32Array) { |
|
36 |
+ try { |
|
37 |
+ var r = new Uint32Array(length); |
|
38 |
+ window.crypto.getRandomValues(r); |
|
39 |
+ randomValues = []; |
|
40 |
+ for (var j=0; j < length; j++) { |
|
41 |
+ randomValues.push(r[j]); |
|
42 |
+ } |
|
43 |
+ } catch(e) { |
|
44 |
+ authLogger.debug("RedirectLoginService.getRandomInts: ", e); |
|
45 |
+ randomValues = null; |
|
46 |
+ } |
|
47 |
+ } |
|
48 |
+ |
|
49 |
+ if (!randomValues) { |
|
50 |
+ randomValues = []; |
|
51 |
+ for (var i=0; i < length; i++) { |
|
52 |
+ randomValues.push(Math.floor(Math.random() * 4294967296)); |
|
53 |
+ } |
|
54 |
+ } |
|
55 |
+ |
|
56 |
+ return randomValues; |
|
57 |
+ }; |
|
58 |
+ |
|
59 |
+ var nonceKey = "RedirectLoginService.nonce"; |
|
60 |
+ var makeState = function(then) { |
|
61 |
+ var nonce = String(new Date().getTime()) + "-" + getRandomInts(8).join(""); |
|
62 |
+ try { |
|
63 |
+ window.localStorage[nonceKey] = nonce; |
|
64 |
+ } catch(e) { |
|
65 |
+ authLogger.log("RedirectLoginService.makeState, localStorage error: ", e); |
|
66 |
+ } |
|
67 |
+ return JSON.stringify({then: then, nonce:nonce}); |
|
68 |
+ }; |
|
69 |
+ var parseState = function(state) { |
|
70 |
+ var retval = { |
|
71 |
+ then: null, |
|
72 |
+ verified: false |
|
73 |
+ }; |
|
74 |
+ |
|
75 |
+ var nonce = ""; |
|
76 |
+ try { |
|
77 |
+ nonce = window.localStorage[nonceKey]; |
|
78 |
+ window.localStorage.removeItem(nonceKey); |
|
79 |
+ } catch(e) { |
|
80 |
+ authLogger.log("RedirectLoginService.parseState, localStorage error: ", e); |
|
81 |
+ } |
|
82 |
+ |
|
83 |
+ try { |
|
84 |
+ var data = state ? JSON.parse(state) : {}; |
|
85 |
+ if (data && data.nonce && nonce && data.nonce === nonce) { |
|
86 |
+ retval.verified = true; |
|
87 |
+ retval.then = data.then; |
|
88 |
+ } |
|
89 |
+ } catch(e) { |
|
90 |
+ authLogger.error("RedirectLoginService.parseState, state error: ", e); |
|
91 |
+ } |
|
92 |
+ authLogger.error("RedirectLoginService.parseState", retval); |
|
93 |
+ return retval; |
|
94 |
+ }; |
|
95 |
+ |
|
32 | 96 |
return { |
33 | 97 |
// Returns a promise that resolves with {user:{...}, token:'...', ttl:X}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} |
34 | 98 |
login: function() { |
... | ... |
@@ -49,7 +113,7 @@ angular.module('openshiftConsole') |
49 | 49 |
uri.query({ |
50 | 50 |
client_id: _oauth_client_id, |
51 | 51 |
response_type: 'token', |
52 |
- state: returnUri.toString(), |
|
52 |
+ state: makeState(returnUri.toString()), |
|
53 | 53 |
redirect_uri: _oauth_redirect_uri |
54 | 54 |
}); |
55 | 55 |
authLogger.log("RedirectLoginService.login(), redirecting", uri.toString()); |
... | ... |
@@ -59,7 +123,7 @@ angular.module('openshiftConsole') |
59 | 59 |
}, |
60 | 60 |
|
61 | 61 |
// Parses oauth callback parameters from window.location |
62 |
- // Returns a promise that resolves with {token:'...',then:'...'}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} |
|
62 |
+ // Returns a promise that resolves with {token:'...',then:'...',verified:true|false}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} |
|
63 | 63 |
// If no token and no error is present, resolves with {} |
64 | 64 |
// Example error codes: https://tools.ietf.org/html/rfc6749#section-5.2 |
65 | 65 |
finish: function() { |
... | ... |
@@ -71,12 +135,12 @@ angular.module('openshiftConsole') |
71 | 71 |
var fragmentParams = new URI("?" + u.fragment()).query(true); |
72 | 72 |
authLogger.log("RedirectLoginService.finish()", queryParams, fragmentParams); |
73 | 73 |
|
74 |
- // Error codes can come in query params or fragment params |
|
75 |
- // Handle an error response from the OAuth server |
|
74 |
+ // Error codes can come in query params or fragment params |
|
75 |
+ // Handle an error response from the OAuth server |
|
76 | 76 |
var error = queryParams.error || fragmentParams.error; |
77 |
- if (error) { |
|
78 |
- var error_description = queryParams.error_description || fragmentParams.error_description; |
|
79 |
- var error_uri = queryParams.error_uri || fragmentParams.error_uri; |
|
77 |
+ if (error) { |
|
78 |
+ var error_description = queryParams.error_description || fragmentParams.error_description; |
|
79 |
+ var error_uri = queryParams.error_uri || fragmentParams.error_uri; |
|
80 | 80 |
authLogger.log("RedirectLoginService.finish(), error", error, error_description, error_uri); |
81 | 81 |
return $q.reject({ |
82 | 82 |
error: error, |
... | ... |
@@ -85,13 +149,16 @@ angular.module('openshiftConsole') |
85 | 85 |
}); |
86 | 86 |
} |
87 | 87 |
|
88 |
+ var stateData = parseState(fragmentParams.state); |
|
89 |
+ |
|
88 | 90 |
// Handle an access_token response |
89 | 91 |
if (fragmentParams.access_token && (fragmentParams.token_type || "").toLowerCase() === "bearer") { |
90 | 92 |
var deferred = $q.defer(); |
91 | 93 |
deferred.resolve({ |
92 | 94 |
token: fragmentParams.access_token, |
93 | 95 |
ttl: fragmentParams.expires_in, |
94 |
- then: fragmentParams.state |
|
96 |
+ then: stateData.state, |
|
97 |
+ verified: stateData.verified |
|
95 | 98 |
}); |
96 | 99 |
return deferred.promise; |
97 | 100 |
} |
... | ... |
@@ -2,12 +2,26 @@ |
2 | 2 |
<div class="wrap no-sidebar"> |
3 | 3 |
<div class="middle surface-shaded"> |
4 | 4 |
<div class="container surface-shaded"> |
5 |
- <div> |
|
5 |
+ <div ng-if="!confirmUser"> |
|
6 | 6 |
<h1 style="margin-top: 10px;">Logging in…</h1> |
7 |
- <div> |
|
8 |
- Please wait while you are logged in... |
|
9 |
- </div> |
|
7 |
+ <p>Please wait while you are logged in…</p> |
|
10 | 8 |
</div> |
9 |
+ |
|
10 |
+ <div ng-if="confirmUser && !overriddenUser"> |
|
11 |
+ <h1 style="margin-top: 10px;">Confirm Login</h1> |
|
12 |
+ <p>You are being logged in as <code>{{confirmUser.metadata.name}}</code>.</p> |
|
13 |
+ <button class="btn btn-lg btn-primary" type="button" ng-click="completeLogin();">Continue</button> |
|
14 |
+ <button class="btn btn-lg btn-default" type="button" ng-click="cancelLogin();">Cancel</button> |
|
15 |
+ </div> |
|
16 |
+ |
|
17 |
+ <div ng-if="confirmUser && overriddenUser"> |
|
18 |
+ <h1 style="margin-top: 10px;">Confirm User Change</h1> |
|
19 |
+ <p>You are about to change users from <code>{{overriddenUser.metadata.name}}</code> to <code>{{confirmUser.metadata.name}}</code>.</p> |
|
20 |
+ <p>If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.</p> |
|
21 |
+ <button class="btn btn-lg btn-danger" type="button" ng-click="completeLogin();">Switch Users</button> |
|
22 |
+ <button class="btn btn-lg btn-primary" type="button" ng-click="cancelLogin();">Cancel</button> |
|
23 |
+ </div> |
|
24 |
+ |
|
11 | 25 |
</div> |
12 | 26 |
</div> |
13 | 27 |
</div> |
... | ... |
@@ -2582,7 +2582,49 @@ return a && (b = a), b; |
2582 | 2582 |
}, this.OAuthRedirectURI = function(a) { |
2583 | 2583 |
return a && (c = a), c; |
2584 | 2584 |
}, this.$get = [ "$location", "$q", "Logger", function(d, e, f) { |
2585 |
-var g = f.get("auth"); |
|
2585 |
+var g = f.get("auth"), h = function(a) { |
|
2586 |
+var b; |
|
2587 |
+if (window.crypto && window.Uint32Array) try { |
|
2588 |
+var c = new Uint32Array(a); |
|
2589 |
+window.crypto.getRandomValues(c), b = []; |
|
2590 |
+for (var d = 0; a > d; d++) b.push(c[d]); |
|
2591 |
+} catch (e) { |
|
2592 |
+g.debug("RedirectLoginService.getRandomInts: ", e), b = null; |
|
2593 |
+} |
|
2594 |
+if (!b) { |
|
2595 |
+b = []; |
|
2596 |
+for (var f = 0; a > f; f++) b.push(Math.floor(4294967296 * Math.random())); |
|
2597 |
+} |
|
2598 |
+return b; |
|
2599 |
+}, i = "RedirectLoginService.nonce", j = function(a) { |
|
2600 |
+var b = String(new Date().getTime()) + "-" + h(8).join(""); |
|
2601 |
+try { |
|
2602 |
+window.localStorage[i] = b; |
|
2603 |
+} catch (c) { |
|
2604 |
+g.log("RedirectLoginService.makeState, localStorage error: ", c); |
|
2605 |
+} |
|
2606 |
+return JSON.stringify({ |
|
2607 |
+then:a, |
|
2608 |
+nonce:b |
|
2609 |
+}); |
|
2610 |
+}, k = function(a) { |
|
2611 |
+var b = { |
|
2612 |
+then:null, |
|
2613 |
+verified:!1 |
|
2614 |
+}, c = ""; |
|
2615 |
+try { |
|
2616 |
+c = window.localStorage[i], window.localStorage.removeItem(i); |
|
2617 |
+} catch (d) { |
|
2618 |
+g.log("RedirectLoginService.parseState, localStorage error: ", d); |
|
2619 |
+} |
|
2620 |
+try { |
|
2621 |
+var e = a ? JSON.parse(a) :{}; |
|
2622 |
+e && e.nonce && c && e.nonce === c && (b.verified = !0, b.then = e.then); |
|
2623 |
+} catch (d) { |
|
2624 |
+g.error("RedirectLoginService.parseState, state error: ", d); |
|
2625 |
+} |
|
2626 |
+return g.error("RedirectLoginService.parseState", b), b; |
|
2627 |
+}; |
|
2586 | 2628 |
return { |
2587 | 2629 |
login:function() { |
2588 | 2630 |
if ("" === a) return e.reject({ |
... | ... |
@@ -2601,7 +2643,7 @@ var f = e.defer(), h = new URI(b), i = new URI(d.url()).fragment(""); |
2601 | 2601 |
return h.query({ |
2602 | 2602 |
client_id:a, |
2603 | 2603 |
response_type:"token", |
2604 |
-state:i.toString(), |
|
2604 |
+state:j(i.toString()), |
|
2605 | 2605 |
redirect_uri:c |
2606 | 2606 |
}), g.log("RedirectLoginService.login(), redirecting", h.toString()), window.location.href = h.toString(), f.promise; |
2607 | 2607 |
}, |
... | ... |
@@ -2617,13 +2659,15 @@ error_description:h, |
2617 | 2617 |
error_uri:i |
2618 | 2618 |
}); |
2619 | 2619 |
} |
2620 |
+var j = k(c.state); |
|
2620 | 2621 |
if (c.access_token && "bearer" === (c.token_type || "").toLowerCase()) { |
2621 |
-var j = e.defer(); |
|
2622 |
-return j.resolve({ |
|
2622 |
+var l = e.defer(); |
|
2623 |
+return l.resolve({ |
|
2623 | 2624 |
token:c.access_token, |
2624 | 2625 |
ttl:c.expires_in, |
2625 |
-then:c.state |
|
2626 |
-}), j.promise; |
|
2626 |
+then:j.state, |
|
2627 |
+verified:j.verified |
|
2628 |
+}), l.promise; |
|
2627 | 2629 |
} |
2628 | 2630 |
return e.reject({ |
2629 | 2631 |
error:"invalid_request", |
... | ... |
@@ -4924,35 +4968,43 @@ hideFilterWidget:!0 |
4924 | 4924 |
}, c.get(a.project).then(_.spread(function(a, c) { |
4925 | 4925 |
b.project = a, b.projectContext = c; |
4926 | 4926 |
})); |
4927 |
-} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f) { |
|
4928 |
-var g = f.get("auth"); |
|
4929 |
-c.finish().then(function(b) { |
|
4930 |
-var c = b.token, f = b.then, h = b.ttl, i = { |
|
4927 |
+} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$scope", "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f, g) { |
|
4928 |
+var h = g.get("auth"); |
|
4929 |
+a.completeLogin = function() {}, a.cancelLogin = function() { |
|
4930 |
+b.replace(), b.url("./"); |
|
4931 |
+}, d.finish().then(function(c) { |
|
4932 |
+var d = c.token, g = c.then, i = c.verified, j = c.ttl, k = { |
|
4931 | 4933 |
errorNotification:!1, |
4932 | 4934 |
http:{ |
4933 | 4935 |
auth:{ |
4934 |
-token:c, |
|
4936 |
+token:d, |
|
4935 | 4937 |
triggerLogin:!1 |
4936 | 4938 |
} |
4937 | 4939 |
} |
4938 | 4940 |
}; |
4939 |
-g.log("OAuthController, got token, fetching user", i), d.get("users", "~", {}, i).then(function(b) { |
|
4940 |
-g.log("OAuthController, got user", b), e.setUser(b, c, h); |
|
4941 |
-var d = f || "./"; |
|
4942 |
-URI(d).is("absolute") && (g.log("OAuthController, invalid absolute redirect", d), d = "./"), g.log("OAuthController, redirecting", d), a.url(d); |
|
4943 |
-})["catch"](function(b) { |
|
4941 |
+h.log("OAuthController, got token, fetching user", k), e.get("users", "~", {}, k).then(function(c) { |
|
4942 |
+if (h.log("OAuthController, got user", c), a.completeLogin = function() { |
|
4943 |
+f.setUser(c, d, j); |
|
4944 |
+var a = g || "./"; |
|
4945 |
+URI(a).is("absolute") && (h.log("OAuthController, invalid absolute redirect", a), a = "./"), h.log("OAuthController, redirecting", a), b.replace(), b.url(a); |
|
4946 |
+}, i) a.completeLogin(); else { |
|
4947 |
+a.confirmUser = c; |
|
4948 |
+var e = f.UserStore().getUser(); |
|
4949 |
+e && e.metadata.name !== c.metadata.name && (a.overriddenUser = e); |
|
4950 |
+} |
|
4951 |
+})["catch"](function(a) { |
|
4944 | 4952 |
var c = URI("error").query({ |
4945 | 4953 |
error:"user_fetch_failed" |
4946 | 4954 |
}).toString(); |
4947 |
-g.error("OAuthController, error fetching user", b, "redirecting", c), a.url(c); |
|
4955 |
+h.error("OAuthController, error fetching user", a, "redirecting", c), b.replace(), b.url(c); |
|
4948 | 4956 |
}); |
4949 |
-})["catch"](function(b) { |
|
4957 |
+})["catch"](function(a) { |
|
4950 | 4958 |
var c = URI("error").query({ |
4951 |
-error:b.error || "", |
|
4952 |
-error_description:b.error_description || "", |
|
4953 |
-error_uri:b.error_uri || "" |
|
4959 |
+error:a.error || "", |
|
4960 |
+error_description:a.error_description || "", |
|
4961 |
+error_uri:a.error_uri || "" |
|
4954 | 4962 |
}).toString(); |
4955 |
-g.error("OAuthController, error", b, "redirecting", c), a.url(c); |
|
4963 |
+h.error("OAuthController, error", a, "redirecting", c), b.replace(), b.url(c); |
|
4956 | 4964 |
}); |
4957 | 4965 |
} ]), angular.module("openshiftConsole").controller("ErrorController", [ "$scope", function(a) { |
4958 | 4966 |
var b = URI(window.location.href).query(!0), c = b.error; |
... | ... |
@@ -14184,11 +14236,22 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', [] |
14184 | 14184 |
"<div class=\"wrap no-sidebar\">\n" + |
14185 | 14185 |
"<div class=\"middle surface-shaded\">\n" + |
14186 | 14186 |
"<div class=\"container surface-shaded\">\n" + |
14187 |
- "<div>\n" + |
|
14187 |
+ "<div ng-if=\"!confirmUser\">\n" + |
|
14188 | 14188 |
"<h1 style=\"margin-top: 10px\">Logging in…</h1>\n" + |
14189 |
- "<div>\n" + |
|
14190 |
- "Please wait while you are logged in...\n" + |
|
14191 |
- "</div>\n" + |
|
14189 |
+ "<p>Please wait while you are logged in…</p>\n" + |
|
14190 |
+ "</div>\n" + |
|
14191 |
+ "<div ng-if=\"confirmUser && !overriddenUser\">\n" + |
|
14192 |
+ "<h1 style=\"margin-top: 10px\">Confirm Login</h1>\n" + |
|
14193 |
+ "<p>You are being logged in as <code>{{confirmUser.metadata.name}}</code>.</p>\n" + |
|
14194 |
+ "<button class=\"btn btn-lg btn-primary\" type=\"button\" ng-click=\"completeLogin();\">Continue</button>\n" + |
|
14195 |
+ "<button class=\"btn btn-lg btn-default\" type=\"button\" ng-click=\"cancelLogin();\">Cancel</button>\n" + |
|
14196 |
+ "</div>\n" + |
|
14197 |
+ "<div ng-if=\"confirmUser && overriddenUser\">\n" + |
|
14198 |
+ "<h1 style=\"margin-top: 10px\">Confirm User Change</h1>\n" + |
|
14199 |
+ "<p>You are about to change users from <code>{{overriddenUser.metadata.name}}</code> to <code>{{confirmUser.metadata.name}}</code>.</p>\n" + |
|
14200 |
+ "<p>If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.</p>\n" + |
|
14201 |
+ "<button class=\"btn btn-lg btn-danger\" type=\"button\" ng-click=\"completeLogin();\">Switch Users</button>\n" + |
|
14202 |
+ "<button class=\"btn btn-lg btn-primary\" type=\"button\" ng-click=\"cancelLogin();\">Cancel</button>\n" + |
|
14192 | 14203 |
"</div>\n" + |
14193 | 14204 |
"</div>\n" + |
14194 | 14205 |
"</div>\n" + |
... | ... |
@@ -2,6 +2,7 @@ package paramtoken |
2 | 2 |
|
3 | 3 |
import ( |
4 | 4 |
"net/http" |
5 |
+ "regexp" |
|
5 | 6 |
"strings" |
6 | 7 |
|
7 | 8 |
"github.com/openshift/origin/pkg/auth/authenticator" |
... | ... |
@@ -26,6 +27,11 @@ func New(param string, auth authenticator.Token, removeParam bool) *Authenticato |
26 | 26 |
} |
27 | 27 |
|
28 | 28 |
func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { |
29 |
+ // Only accept query param auth for websocket connections |
|
30 |
+ if !isWebSocketRequest(req) { |
|
31 |
+ return nil, false, nil |
|
32 |
+ } |
|
33 |
+ |
|
29 | 34 |
q := req.URL.Query() |
30 | 35 |
token := strings.TrimSpace(q.Get(a.param)) |
31 | 36 |
if token == "" { |
... | ... |
@@ -38,3 +44,13 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, |
38 | 38 |
} |
39 | 39 |
return user, ok, err |
40 | 40 |
} |
41 |
+ |
|
42 |
+var ( |
|
43 |
+ // connectionUpgradeRegex matches any Connection header value that includes upgrade |
|
44 |
+ connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)") |
|
45 |
+) |
|
46 |
+ |
|
47 |
+// isWebSocketRequest returns true if the incoming request contains connection upgrade headers for WebSockets. |
|
48 |
+func isWebSocketRequest(req *http.Request) bool { |
|
49 |
+ return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket" |
|
50 |
+} |
... | ... |
@@ -5,15 +5,14 @@ import ( |
5 | 5 |
"strings" |
6 | 6 |
|
7 | 7 |
kapi "k8s.io/kubernetes/pkg/api" |
8 |
- kapiserver "k8s.io/kubernetes/pkg/apiserver" |
|
9 | 8 |
) |
10 | 9 |
|
11 | 10 |
type openshiftAuthorizationAttributeBuilder struct { |
12 | 11 |
contextMapper kapi.RequestContextMapper |
13 |
- infoResolver *kapiserver.RequestInfoResolver |
|
12 |
+ infoResolver RequestInfoResolver |
|
14 | 13 |
} |
15 | 14 |
|
16 |
-func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver *kapiserver.RequestInfoResolver) AuthorizationAttributeBuilder { |
|
15 |
+func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver RequestInfoResolver) AuthorizationAttributeBuilder { |
|
17 | 16 |
return &openshiftAuthorizationAttributeBuilder{contextMapper, infoResolver} |
18 | 17 |
} |
19 | 18 |
|
20 | 19 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,75 @@ |
0 |
+package authorizer |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "net/http" |
|
4 |
+ |
|
5 |
+ kapi "k8s.io/kubernetes/pkg/api" |
|
6 |
+ kapiserver "k8s.io/kubernetes/pkg/apiserver" |
|
7 |
+ "k8s.io/kubernetes/pkg/util/sets" |
|
8 |
+) |
|
9 |
+ |
|
10 |
+type browserSafeRequestInfoResolver struct { |
|
11 |
+ // infoResolver is used to determine info for the request |
|
12 |
+ infoResolver RequestInfoResolver |
|
13 |
+ |
|
14 |
+ // contextMapper is used to look up the context corresponding to a request |
|
15 |
+ // to obtain the user associated with the request |
|
16 |
+ contextMapper kapi.RequestContextMapper |
|
17 |
+ |
|
18 |
+ // list of groups, any of which indicate the request is authenticated |
|
19 |
+ authenticatedGroups sets.String |
|
20 |
+} |
|
21 |
+ |
|
22 |
+func NewBrowserSafeRequestInfoResolver(contextMapper kapi.RequestContextMapper, authenticatedGroups sets.String, infoResolver RequestInfoResolver) RequestInfoResolver { |
|
23 |
+ return &browserSafeRequestInfoResolver{ |
|
24 |
+ contextMapper: contextMapper, |
|
25 |
+ authenticatedGroups: authenticatedGroups, |
|
26 |
+ infoResolver: infoResolver, |
|
27 |
+ } |
|
28 |
+} |
|
29 |
+ |
|
30 |
+func (a *browserSafeRequestInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) { |
|
31 |
+ requestInfo, err := a.infoResolver.GetRequestInfo(req) |
|
32 |
+ if err != nil { |
|
33 |
+ return requestInfo, err |
|
34 |
+ } |
|
35 |
+ |
|
36 |
+ if !requestInfo.IsResourceRequest { |
|
37 |
+ return requestInfo, nil |
|
38 |
+ } |
|
39 |
+ |
|
40 |
+ isProxyVerb := requestInfo.Verb == "proxy" |
|
41 |
+ isProxySubresource := requestInfo.Subresource == "proxy" |
|
42 |
+ |
|
43 |
+ if !isProxyVerb && !isProxySubresource { |
|
44 |
+ // Requests to non-proxy resources don't expose HTML or HTTP-handling user content to browsers |
|
45 |
+ return requestInfo, nil |
|
46 |
+ } |
|
47 |
+ |
|
48 |
+ if len(req.Header.Get("X-CSRF-Token")) > 0 { |
|
49 |
+ // Browsers cannot set custom headers on direct requests |
|
50 |
+ return requestInfo, nil |
|
51 |
+ } |
|
52 |
+ |
|
53 |
+ if ctx, hasContext := a.contextMapper.Get(req); hasContext { |
|
54 |
+ user, hasUser := kapi.UserFrom(ctx) |
|
55 |
+ if hasUser && a.authenticatedGroups.HasAny(user.GetGroups()...) { |
|
56 |
+ // An authenticated request indicates this isn't a browser page load. |
|
57 |
+ // Browsers cannot make direct authenticated requests. |
|
58 |
+ // This depends on the API not enabling basic or cookie-based auth. |
|
59 |
+ return requestInfo, nil |
|
60 |
+ } |
|
61 |
+ |
|
62 |
+ } |
|
63 |
+ |
|
64 |
+ // TODO: compare request.Host to a list of hosts allowed for the requestInfo.Namespace (e.g. <namespace>.proxy.example.com) |
|
65 |
+ |
|
66 |
+ if isProxyVerb { |
|
67 |
+ requestInfo.Verb = "unsafeproxy" |
|
68 |
+ } |
|
69 |
+ if isProxySubresource { |
|
70 |
+ requestInfo.Subresource = "unsafeproxy" |
|
71 |
+ } |
|
72 |
+ |
|
73 |
+ return requestInfo, nil |
|
74 |
+} |
0 | 75 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,147 @@ |
0 |
+package authorizer |
|
1 |
+ |
|
2 |
+import ( |
|
3 |
+ "net/http" |
|
4 |
+ "testing" |
|
5 |
+ |
|
6 |
+ kapi "k8s.io/kubernetes/pkg/api" |
|
7 |
+ kapiserver "k8s.io/kubernetes/pkg/apiserver" |
|
8 |
+ "k8s.io/kubernetes/pkg/auth/user" |
|
9 |
+ "k8s.io/kubernetes/pkg/util/sets" |
|
10 |
+) |
|
11 |
+ |
|
12 |
+func TestUpstreamInfoResolver(t *testing.T) { |
|
13 |
+ subresourceRequest, _ := http.NewRequest("GET", "/api/v1/namespaces/myns/pods/mypod/proxy", nil) |
|
14 |
+ proxyRequest, _ := http.NewRequest("GET", "/api/v1/proxy/nodes/mynode", nil) |
|
15 |
+ |
|
16 |
+ testcases := map[string]struct { |
|
17 |
+ Request *http.Request |
|
18 |
+ ExpectedVerb string |
|
19 |
+ ExpectedSubresource string |
|
20 |
+ }{ |
|
21 |
+ "unsafe proxy subresource": { |
|
22 |
+ Request: subresourceRequest, |
|
23 |
+ ExpectedVerb: "get", |
|
24 |
+ ExpectedSubresource: "proxy", // should be "unsafeproxy" or similar once check moves upstream |
|
25 |
+ }, |
|
26 |
+ "unsafe proxy verb": { |
|
27 |
+ Request: proxyRequest, |
|
28 |
+ ExpectedVerb: "proxy", // should be "unsafeproxy" or similar once check moves upstream |
|
29 |
+ }, |
|
30 |
+ } |
|
31 |
+ |
|
32 |
+ for k, tc := range testcases { |
|
33 |
+ resolver := &kapiserver.RequestInfoResolver{ |
|
34 |
+ APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), |
|
35 |
+ GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi"), |
|
36 |
+ } |
|
37 |
+ |
|
38 |
+ info, err := resolver.GetRequestInfo(tc.Request) |
|
39 |
+ if err != nil { |
|
40 |
+ t.Errorf("%s: unexpected error: %v", k, err) |
|
41 |
+ continue |
|
42 |
+ } |
|
43 |
+ |
|
44 |
+ if info.Verb != tc.ExpectedVerb { |
|
45 |
+ t.Errorf("%s: expected verb %s, got %s. If kapiserver.RequestInfoResolver now adjusts attributes for proxy safety, investigate removing the NewBrowserSafeRequestInfoResolver wrapper.", k, tc.ExpectedVerb, info.Verb) |
|
46 |
+ } |
|
47 |
+ if info.Subresource != tc.ExpectedSubresource { |
|
48 |
+ t.Errorf("%s: expected verb %s, got %s. If kapiserver.RequestInfoResolver now adjusts attributes for proxy safety, investigate removing the NewBrowserSafeRequestInfoResolver wrapper.", k, tc.ExpectedSubresource, info.Subresource) |
|
49 |
+ } |
|
50 |
+ } |
|
51 |
+} |
|
52 |
+ |
|
53 |
+func TestBrowserSafeRequestInfoResolver(t *testing.T) { |
|
54 |
+ testcases := map[string]struct { |
|
55 |
+ RequestInfo kapiserver.RequestInfo |
|
56 |
+ Context kapi.Context |
|
57 |
+ Host string |
|
58 |
+ Headers http.Header |
|
59 |
+ |
|
60 |
+ ExpectedVerb string |
|
61 |
+ ExpectedSubresource string |
|
62 |
+ }{ |
|
63 |
+ "non-resource": { |
|
64 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: false, Verb: "GET"}, |
|
65 |
+ ExpectedVerb: "GET", |
|
66 |
+ }, |
|
67 |
+ |
|
68 |
+ "non-proxy": { |
|
69 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "logs"}, |
|
70 |
+ ExpectedVerb: "get", |
|
71 |
+ ExpectedSubresource: "logs", |
|
72 |
+ }, |
|
73 |
+ |
|
74 |
+ "unsafe proxy subresource": { |
|
75 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, |
|
76 |
+ ExpectedVerb: "get", |
|
77 |
+ ExpectedSubresource: "unsafeproxy", |
|
78 |
+ }, |
|
79 |
+ "unsafe proxy verb": { |
|
80 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"}, |
|
81 |
+ ExpectedVerb: "unsafeproxy", |
|
82 |
+ }, |
|
83 |
+ "unsafe proxy verb anonymous": { |
|
84 |
+ Context: kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "system:anonymous", Groups: []string{"system:unauthenticated"}}), |
|
85 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"}, |
|
86 |
+ ExpectedVerb: "unsafeproxy", |
|
87 |
+ }, |
|
88 |
+ |
|
89 |
+ "proxy subresource authenticated": { |
|
90 |
+ Context: kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "bob", Groups: []string{"system:authenticated"}}), |
|
91 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, |
|
92 |
+ ExpectedVerb: "get", |
|
93 |
+ ExpectedSubresource: "proxy", |
|
94 |
+ }, |
|
95 |
+ "proxy subresource custom header": { |
|
96 |
+ RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, |
|
97 |
+ Headers: http.Header{"X-Csrf-Token": []string{"1"}}, |
|
98 |
+ ExpectedVerb: "get", |
|
99 |
+ ExpectedSubresource: "proxy", |
|
100 |
+ }, |
|
101 |
+ } |
|
102 |
+ |
|
103 |
+ for k, tc := range testcases { |
|
104 |
+ resolver := NewBrowserSafeRequestInfoResolver( |
|
105 |
+ &testContextMapper{tc.Context}, |
|
106 |
+ sets.NewString("system:authenticated"), |
|
107 |
+ &testInfoResolver{tc.RequestInfo}, |
|
108 |
+ ) |
|
109 |
+ |
|
110 |
+ req, _ := http.NewRequest("GET", "/", nil) |
|
111 |
+ req.Host = tc.Host |
|
112 |
+ req.Header = tc.Headers |
|
113 |
+ |
|
114 |
+ info, err := resolver.GetRequestInfo(req) |
|
115 |
+ if err != nil { |
|
116 |
+ t.Errorf("%s: unexpected error: %v", k, err) |
|
117 |
+ continue |
|
118 |
+ } |
|
119 |
+ |
|
120 |
+ if info.Verb != tc.ExpectedVerb { |
|
121 |
+ t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedVerb, info.Verb) |
|
122 |
+ } |
|
123 |
+ if info.Subresource != tc.ExpectedSubresource { |
|
124 |
+ t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedSubresource, info.Subresource) |
|
125 |
+ } |
|
126 |
+ } |
|
127 |
+} |
|
128 |
+ |
|
129 |
+type testContextMapper struct { |
|
130 |
+ context kapi.Context |
|
131 |
+} |
|
132 |
+ |
|
133 |
+func (t *testContextMapper) Get(req *http.Request) (kapi.Context, bool) { |
|
134 |
+ return t.context, t.context != nil |
|
135 |
+} |
|
136 |
+func (t *testContextMapper) Update(req *http.Request, ctx kapi.Context) error { |
|
137 |
+ return nil |
|
138 |
+} |
|
139 |
+ |
|
140 |
+type testInfoResolver struct { |
|
141 |
+ info kapiserver.RequestInfo |
|
142 |
+} |
|
143 |
+ |
|
144 |
+func (t *testInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) { |
|
145 |
+ return t.info, nil |
|
146 |
+} |
... | ... |
@@ -4,6 +4,7 @@ import ( |
4 | 4 |
"net/http" |
5 | 5 |
|
6 | 6 |
kapi "k8s.io/kubernetes/pkg/api" |
7 |
+ kapiserver "k8s.io/kubernetes/pkg/apiserver" |
|
7 | 8 |
"k8s.io/kubernetes/pkg/auth/user" |
8 | 9 |
"k8s.io/kubernetes/pkg/util/sets" |
9 | 10 |
) |
... | ... |
@@ -17,6 +18,10 @@ type AuthorizationAttributeBuilder interface { |
17 | 17 |
GetAttributes(request *http.Request) (AuthorizationAttributes, error) |
18 | 18 |
} |
19 | 19 |
|
20 |
+type RequestInfoResolver interface { |
|
21 |
+ GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) |
|
22 |
+} |
|
23 |
+ |
|
20 | 24 |
type AuthorizationAttributes interface { |
21 | 25 |
GetVerb() string |
22 | 26 |
GetAPIVersion() string |
... | ... |
@@ -375,7 +375,16 @@ func newAuthorizer(policyClient policyclient.ReadOnlyPolicyClient, projectReques |
375 | 375 |
} |
376 | 376 |
|
377 | 377 |
func newAuthorizationAttributeBuilder(requestContextMapper kapi.RequestContextMapper) authorizer.AuthorizationAttributeBuilder { |
378 |
- authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")}) |
|
378 |
+ // Default API request resolver |
|
379 |
+ requestInfoResolver := &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")} |
|
380 |
+ // Wrap with a resolver that detects unsafe requests and modifies verbs/resources appropriately so policy can address them separately |
|
381 |
+ browserSafeRequestInfoResolver := authorizer.NewBrowserSafeRequestInfoResolver( |
|
382 |
+ requestContextMapper, |
|
383 |
+ sets.NewString(bootstrappolicy.AuthenticatedGroup), |
|
384 |
+ requestInfoResolver, |
|
385 |
+ ) |
|
386 |
+ |
|
387 |
+ authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, browserSafeRequestInfoResolver) |
|
379 | 388 |
return authorizationAttributeBuilder |
380 | 389 |
} |
381 | 390 |
|