Browse code

Add login csrf prompts

Jordan Liggitt authored on 2016/04/26 00:11:46
Showing 4 changed files
... ...
@@ -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&hellip;</h1>
7
-        <div>
8
-          Please wait while you are logged in...
9
-        </div>
7
+        <p>Please wait while you are logged in&hellip;</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&hellip;</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&hellip;</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" +