You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ja...@apache.org on 2023/09/13 22:04:28 UTC

[solr] branch main updated: add support of OAuth 2.0/OIDC 'code with PKCE' flow (front-end) (#1791)

This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 086dcbe960f add support of OAuth 2.0/OIDC 'code with PKCE' flow (front-end) (#1791)
086dcbe960f is described below

commit 086dcbe960ff6cd8f604e5b988972f8cfc1ef0b8
Author: Lamine <10...@users.noreply.github.com>
AuthorDate: Wed Sep 13 17:04:22 2023 -0500

    add support of OAuth 2.0/OIDC 'code with PKCE' flow (front-end) (#1791)
    
    Co-authored-by: Lamine Idjeraoui <li...@apple.com>
    Co-authored-by: Jan Høydahl <ja...@users.noreply.github.com>
---
 NOTICE.txt                                      |   7 +
 solr/CHANGES.txt                                |   2 +-
 solr/webapp/web/index.html                      |   1 +
 solr/webapp/web/js/angular/controllers/login.js | 267 ++++++++++++++++--------
 solr/webapp/web/js/angular/services.js          |  78 ++++++-
 solr/webapp/web/libs/jssha-3.3.1-sha256.min.js  |  24 +++
 6 files changed, 291 insertions(+), 88 deletions(-)

diff --git a/NOTICE.txt b/NOTICE.txt
index 396db11e6cb..0fa2229e28a 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -618,6 +618,13 @@ https://github.com/yonik/noggit
 This product includes the Angular UI UI Grid JavaScript library.
 Copyright (c) 2015 the AngularUI Team, http://angular-ui.github.com
 
+=========================================================================
+==         jsSHA notice                                                ==
+=========================================================================
+
+This product includes the jsSHA library.
+Copyright (c) 2008-2023 Brian Turek, 1998-2009 Paul Johnston & Contributors
+https://github.com/Caligatio/jsSHA
 
 =========================================================================
 ==         grpc notice                                                 ==
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 93648908345..7d5c3776f5c 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -111,7 +111,7 @@ Improvements
 
 * SOLR-16927: Allow SolrClientCache clients to use Jetty HTTP2 clients (Alex Deparvu, David Smiley)
 
-* SOLR-16896, SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (Lamine Idjeraoui, janhoy, Kevin Risden)
+* SOLR-16896, SOLR-16897: Add support of OAuth 2.0/OIDC 'code with PKCE' flow (Lamine Idjeraoui, janhoy, Kevin Risden, Anshum Gupta)
 
 * SOLR-16879: Limit the number of concurrent expensive core admin operations by running them in a
   dedicated thread pool. Backup, Restore and Split are expensive operations.
diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html
index afffe67f764..35b1d664860 100644
--- a/solr/webapp/web/index.html
+++ b/solr/webapp/web/index.html
@@ -70,6 +70,7 @@ limitations under the License.
   <script src="libs/ui-grid.min.js?_=${version}"></script>
   <script src="libs/jquery-ui.min.js?_=${version}"></script>
   <script src="libs/angular-utf8-base64.min.js?_=${version}"></script>
+  <script src="libs/jssha-3.3.1-sha256.min.js?_=${version}"></script>
   <script src="js/angular/app.js?_=${version}"></script>
   <script src="js/angular/services.js?_=${version}"></script>
   <script src="js/angular/permissions.js?_=${version}"></script>
diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js
index b76ec1f4a8a..0e22100c306 100644
--- a/solr/webapp/web/js/angular/controllers/login.js
+++ b/solr/webapp/web/js/angular/controllers/login.js
@@ -60,92 +60,166 @@ solrAdminApp.controller('LoginController',
             var hp = AuthenticationService.decodeHashParams(hash);
             var expectedState = sessionStorage.getItem("auth.stateRandom") + "_" + sessionStorage.getItem("auth.location");
             sessionStorage.setItem("auth.state", "error");
-            if (hp['access_token'] && hp['token_type'] && hp['state']) {
-              // Validate state
-              if (hp['state'] !== expectedState) {
-                $scope.error = "Problem with auth callback";
-                console.log("Expected state param " + expectedState + " but got " + hp['state']);
-                errorText += "Invalid values in state parameter. ";
-              }
-              // Validate token type
-              if (hp['token_type'].toLowerCase() !== "bearer") {
-                console.log("Expected token_type param 'bearer', but got " + hp['token_type']);
-                errorText += "Invalid values in token_type parameter. ";
-              }
-              // Unpack ID token and validate nonce, get username
-              if (hp['id_token']) {
-                var idToken = hp['id_token'].split(".");
-                if (idToken.length === 3) {
-                  var payload = AuthenticationService.decodeJwtPart(idToken[1]);
-                  if (!payload['nonce'] || payload['nonce'] !== sessionStorage.getItem("auth.nonce")) {
-                    errorText += "Invalid 'nonce' value, possible attack detected. Please log in again. ";
-                  }
+            $scope.authData = AuthenticationService.getAuthDataHeader();
+            if (!validateState(hp['state'], expectedState)) {
+              $scope.error = "Problems with OpenID callback";
+              $scope.errorDescription = errorText;
+              $scope.http401 = "true";
+              sessionStorage.setItem("auth.state", "error");
+            }
+            else {
+              var flow = $scope.authData ? $scope.authData['authorization_flow'] : undefined;
+              console.log("Callback: authorization_flow : " +flow);
+              var isCodePKCE = flow == 'code_pkce';
+              if (isCodePKCE) {
+                // code flow with PKCE
+                var code = hp['code'];
+                var tokenEndpoint = $scope.authData['tokenEndpoint'];
+                // concurrent Solr API calls will trigger 401 and erase session's "auth.realm" in app.js
+                // save it before it's erased
+                var authRealm = sessionStorage.getItem("auth.realm");
+
+                var data = {
+                  'grant_type': 'authorization_code',
+                  'code': code,
+                  'redirect_uri': $window.location.href.split('#')[0],
+                  'scope': "openid " + $scope.authData['scope'],
+                  'code_verifier': sessionStorage.getItem('codeVerifier'),
+                  "client_id": $scope.authData['client_id']
+                };
 
-                  if (errorText === "") {
-                    sessionStorage.setItem("auth.username", payload['sub']);
-                    sessionStorage.setItem("auth.header", "Bearer " + hp['access_token']);
-                    sessionStorage.removeItem("auth.statusText");
-                    sessionStorage.removeItem("auth.stateRandom");
-                    sessionStorage.removeItem("auth.wwwAuthHeader");
-                    console.log("User " + payload['sub'] + " is logged in");
-                    var redirectTo = sessionStorage.getItem("auth.location");
-                    console.log("Redirecting to stored location " + redirectTo);
-                    sessionStorage.setItem("auth.state", "authenticated");
-                    sessionStorage.removeItem("http401");
-                    $location.path(redirectTo).hash("");
+                console.debug(`Callback. Got code: ${code} \nCalling token endpoint:: ${tokenEndpoint} `);
+                AuthenticationService.getOAuthTokens(tokenEndpoint, data).then(function(response) {
+                    var accessToken = response.access_token;
+                    var idToken = response.id_token;
+                    var tokenType = response.access_type;
+                    sessionStorage.setItem("auth.realm", authRealm);
+                    processTokensResponse(accessToken, idToken, tokenType, expectedState, hp);
+                }).catch(function (error) {
+                  errorText += "Error calling token endpoint. ";
+                  $scope.error = "Problems with OpenID callback";
+                  $scope.errorDescription = errorText;
+                  $scope.http401 = "true";
+                  sessionStorage.setItem("auth.state", "error");
+                  if (error && error.data) {
+                    console.error("Error getting tokens: " + JSON.stringify(error.data));
+                  } else {
+                    console.error("Error getting tokens: " + error);
                   }
-                } else {
-                  console.log("Expected JWT compact id_token param but got " + idToken);
-                  errorText += "Invalid values in id_token parameter. ";
-                }
-              } else {
-                console.log("Callback was missing the id_token parameter, could not validate nonce.");
-                errorText += "Callback was missing the id_token parameter, could not validate nonce. ";
-              }
-              if (hp['access_token'].split(".").length !== 3) {
-                console.log("Expected JWT compact access_token param but got " + hp['access_token']);
-                errorText += "Invalid values in access_token parameter. ";
-              }
-              if (errorText !== "") {
-                $scope.error = "Problems with OpenID callback";
-                $scope.errorDescription = errorText;
-                $scope.http401 = "true";
+                });
               }
-              // End callback processing
-            } else if (hp['error']) {
-              // The callback had errors
-              console.log("Error received from idp: " + hp['error']);
-              var errorDescriptions = {};
-              errorDescriptions['invalid_request'] = "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.";
-              errorDescriptions['unauthorized_client'] = "The client is not authorized to request an access token using this method.";
-              errorDescriptions['access_denied'] = "The resource owner or authorization server denied the request.";
-              errorDescriptions['unsupported_response_type'] = "The authorization server does not support obtaining an access token using this method.";
-              errorDescriptions['invalid_scope'] = "The requested scope is invalid, unknown, or malformed.";
-              errorDescriptions['server_error'] = "The authorization server encountered an unexpected condition that prevented it from fulfilling the request.";
-              errorDescriptions['temporarily_unavailable'] = "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.";
-              $scope.error = "Callback from Id Provider contained error. ";
-              if (hp['error_description']) {
-                $scope.errorDescription = decodeURIComponent(hp['error_description']);
-              } else {
-                $scope.errorDescription = errorDescriptions[hp['error']];
+              else {
+                // implicit flow
+                processTokensResponse(hp['access_token'], hp['id_token'], hp['token_type'], expectedState, hp);
               }
-              if (hp['error_uri']) {
-                $scope.errorDescription += " More information at " + hp['error_uri'] + ". ";
+            }
+        }
+      }
+
+      function validateState(state, expectedState) {
+        if (state !== expectedState) {
+          $scope.error = "Problem with auth callback";
+          console.error("Expected state param " + expectedState + " but got " + state);
+          errorText += "Invalid values in state parameter. ";
+          return false;
+        }
+        return true;
+      }
+
+      function processTokensResponse(accessToken, idToken, tokenType, expectedState, hp) {
+        if (accessToken && hp['state']) {
+          // Validate token type.
+          if (!tokenType) {
+            //Assume the type is 'bearer' if it's not returned. Most IdProviders support 'bearer' by default but don't always return the type.
+            tokenType = "bearer";
+          }
+          else if(tokenType.toLowerCase() !== "bearer") {
+            console.error("Expected token_type param 'bearer', but got " + tokenType);
+            errorText += "Invalid values in token_type parameter. ";
+          }
+          // Unpack ID token and validate nonce, get username
+          if (idToken) {
+            var idTokenArray = idToken.split(".");
+            if (idTokenArray.length === 3) {
+              var payload = AuthenticationService.decodeJwtPart(idTokenArray[1]);
+              if (!payload['nonce'] || payload['nonce'] !== sessionStorage.getItem("auth.nonce")) {
+                errorText += "Invalid 'nonce' value, possible attack detected. Please log in again. ";
               }
-              if (hp['state'] !== expectedState) {
-                $scope.errorDescription += "The state parameter returned from ID Provider did not match the one we sent.";
+
+              if (errorText === "") {
+                sessionStorage.setItem("auth.username", payload['sub']);
+                sessionStorage.setItem("auth.header", "Bearer " + accessToken);
+                sessionStorage.removeItem("auth.statusText");
+                sessionStorage.removeItem("auth.stateRandom");
+                sessionStorage.removeItem("auth.wwwAuthHeader");
+                console.log("User " + payload['sub'] + " is logged in");
+                var redirectTo = sessionStorage.getItem("auth.location");
+                console.log("Redirecting to stored location " + redirectTo);
+                sessionStorage.setItem("auth.state", "authenticated");
+                sessionStorage.removeItem("http401");
+                sessionStorage.setItem("auth.scheme", "Bearer");
+                $location.path(redirectTo).hash("");
               }
-              sessionStorage.setItem("auth.state", "error");
+            } else {
+              console.error("Expected JWT compact id_token param but got " + idTokenArray);
+              errorText += "Invalid values in id_token parameter. ";
             }
+          } else {
+            console.error("Callback was missing the id_token parameter, could not validate nonce.");
+            errorText += "Callback was missing the id_token parameter, could not validate nonce. ";
+          }
+          if (accessToken.split(".").length !== 3) {
+            console.error("Expected JWT compact access_token param but got " + accessToken);
+            errorText += "Invalid values in access_token parameter. ";
+          }
+          if (errorText !== "") {
+            $scope.error = "Problems with OpenID callback";
+            $scope.errorDescription = errorText;
+            $scope.http401 = "true";
+          }
+          // End callback processing
+        } else if (hp['error']) {
+          // The callback had errors
+          console.error("Error received from idp: " + hp['error']);
+          var errorDescriptions = {};
+          errorDescriptions['invalid_request'] = "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.";
+          errorDescriptions['unauthorized_client'] = "The client is not authorized to request an access token using this method.";
+          errorDescriptions['access_denied'] = "The resource owner or authorization server denied the request.";
+          errorDescriptions['unsupported_response_type'] = "The authorization server does not support obtaining an access token using this method.";
+          errorDescriptions['invalid_scope'] = "The requested scope is invalid, unknown, or malformed.";
+          errorDescriptions['server_error'] = "The authorization server encountered an unexpected condition that prevented it from fulfilling the request.";
+          errorDescriptions['temporarily_unavailable'] = "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.";
+          $scope.error = "Callback from Id Provider contained error. ";
+          if (hp['error_description']) {
+            $scope.errorDescription = decodeURIComponent(hp['error_description']);
+          } else {
+            $scope.errorDescription = errorDescriptions[hp['error']];
+          }
+          if (hp['error_uri']) {
+            $scope.errorDescription += " More information at " + hp['error_uri'] + ". ";
           }
+          if (hp['state'] !== expectedState) {
+            $scope.errorDescription += "The state parameter returned from ID Provider did not match the one we sent.";
+          }
+          sessionStorage.setItem("auth.state", "error");
+        }
+        else{
+          console.error(`Invalid data received from idp: accessToken: ${accessToken},
+                      idToken: ${idToken}, state: ${hp['state']}`);
+          errorText += "Invalid data received from the OpenID provider. ";
+          $scope.http401 = "true";
+          $scope.error = "Problems with OpenID callback.";
+          $scope.errorDescription = errorText;
+          sessionStorage.setItem("auth.state", "error");
         }
+      }
 
         if (errorText === "" && !$scope.error && authParams) {
           $scope.error = authParams['error'];
           $scope.errorDescription = authParams['error_description'];
           $scope.authData = AuthenticationService.getAuthDataHeader();
         }
-        
+
         $scope.authScheme = sessionStorage.getItem("auth.scheme");
         $scope.authRealm = sessionStorage.getItem("auth.realm");
         $scope.wwwAuthHeader = sessionStorage.getItem("auth.wwwAuthHeader");
@@ -165,20 +239,49 @@ solrAdminApp.controller('LoginController',
           $location.path("/");
         };
 
-        $scope.jwtLogin = function () {
+        $scope.jwtLogin = async function () {
           var stateRandom = Math.random().toString(36).substr(2);
           sessionStorage.setItem("auth.stateRandom", stateRandom);
           var authState = stateRandom + "_" + sessionStorage.getItem("auth.location");
           var authNonce = Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2);
           sessionStorage.setItem("auth.nonce", authNonce);
-          var params = {
-            "response_type" : "id_token token",
-            "client_id" : $scope.authData['client_id'],
-            "redirect_uri" : $window.location.href.split('#')[0],
-            "scope" : "openid " + $scope.authData['scope'],
-            "state" : authState,
-            "nonce" : authNonce
-          };
+          var authData = AuthenticationService.getAuthDataHeader();
+          var flow = authData ? authData['authorization_flow'] : "implicit";
+          console.log("jwtLogin flow: "+ flow);
+          var isCodePKCE = flow == 'code_pkce';
+
+          var params = {};
+          if (isCodePKCE) {
+            console.debug("Login with 'Code PKCE' flow");
+            var codeVerifier = AuthenticationService.generateCodeVerifier();
+            var code_challenge = await AuthenticationService.generateCodeChallengeFromVerifier(codeVerifier);
+            var codeChallengeMethod = AuthenticationService.getCodeChallengeMethod();
+            sessionStorage.setItem('codeVerifier', codeVerifier);
+            params = {
+              "response_type": "code",
+              "client_id": $scope.authData['client_id'],
+              "redirect_uri": $window.location.href.split('#')[0],
+              "scope": "openid " + $scope.authData['scope'],
+              "state": authState,
+              "nonce": authNonce,
+              "response_mode": "fragment",
+              "code_challenge": code_challenge,
+              "code_challenge_method": codeChallengeMethod
+            };
+          }
+          else {
+            console.debug("Login with 'Implicit' flow");
+            params = {
+              "response_type": "id_token token",
+              "client_id": $scope.authData['client_id'],
+              "redirect_uri": $window.location.href.split('#')[0],
+              "scope": "openid " + $scope.authData['scope'],
+              "state": authState,
+              "nonce": authNonce,
+              "response_mode": 'fragment',
+              "grant_type": 'implicit'
+            };
+          }
 
           var endpointBaseUrl = $scope.authData['authorizationEndpoint'];
           var loc = endpointBaseUrl + "?" + paramsToString(params);
@@ -191,7 +294,7 @@ solrAdminApp.controller('LoginController',
             for (var p in params) {
                if( params.hasOwnProperty(p) ) {
                  arr.push(p + "=" + encodeURIComponent(params[p]));
-               } 
+               }
              }
              return arr.join("&");
           }
@@ -204,7 +307,7 @@ solrAdminApp.controller('LoginController',
             redirect.forEach(function(uri) { // Check that current node URL is among the configured callback URIs
               if ($window.location.href.startsWith(uri)) isLoginNode = true;
             });
-            return isLoginNode; 
+            return isLoginNode;
           } else {
             return true; // no redirect UIRs configured, all nodes are potential login nodes
           }
diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js
index 1c0b702dbe1..2b870ab53ff 100644
--- a/solr/webapp/web/js/angular/services.js
+++ b/solr/webapp/web/js/angular/services.js
@@ -286,8 +286,75 @@ solrAdminServices.factory('System',
         })
 }])
 .factory('AuthenticationService',
-    ['base64', function (base64) {
-        var service = {};
+    ['base64', '$resource', function (base64, $resource) {
+      var service = {};
+
+      service.getOAuthTokens = function (url, data) {
+        var serializedData = serialize(data);
+        var resource = $resource(url, {}, {
+          getToken: {
+            method: 'POST',
+            timeout: 10000,
+            headers: {
+              'Content-Type': 'application/x-www-form-urlencoded',
+              'X-Requested-With': undefined // Set this header to undefined to prevent preflight requests
+            },
+            transformResponse: function (data) {
+              return angular.fromJson(data);
+            }
+          }
+        });
+        return resource.getToken({}, serializedData).$promise;
+      };
+
+      var codeChallengeMethod = "S256";
+      service.getCodeChallengeMethod = function getCodeChallengeMethod() {
+        return codeChallengeMethod;
+      }
+
+      service.generateCodeVerifier = function generateCodeVerifier() {
+        var codeVerifier = '';
+        var possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
+        for (var i = 0; i < 96; i++) {
+          codeVerifier += possibleChars.charAt(Math.floor(Math.random() * possibleChars.length));
+        }
+        return codeVerifier;
+      }
+
+      service.generateCodeChallengeFromVerifier = async function generateCodeChallengeFromVerifier(v) {
+        var hashed = await sha256(v);
+        var base64encoded = base64urlencode(hashed);
+        return base64encoded;
+      }
+
+      function sha256(str) {
+        const shaObj = new jsSHA("SHA-256", "TEXT", { encoding: "UTF8" });
+        shaObj.update(str);
+        return shaObj.getHash("UINT8ARRAY");
+      }
+
+      function base64urlencode(a) {
+        var str = "";
+        var bytes = new Uint8Array(a);
+        var len = bytes.byteLength;
+        for (var i = 0; i < len; i++) {
+          str += String.fromCharCode(bytes[i]);
+        }
+        return btoa(str)
+          .replace(/\+/g, "-")
+          .replace(/\//g, "_")
+          .replace(/=+$/, "");
+      }
+
+      var serialize = function (obj) {
+        var str = [];
+        for (var p in obj) {
+          if (obj.hasOwnProperty(p)) {
+            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
+          }
+        }
+        return str.join("&");
+      };
 
         service.SetCredentials = function (username, password) {
           var authdata = base64.encode(username + ':' + password);
@@ -305,6 +372,7 @@ solrAdminServices.factory('System',
           sessionStorage.removeItem("auth.statusText");
           localStorage.removeItem("auth.stateRandom");
           sessionStorage.removeItem("auth.nonce");
+          sessionStorage.removeItem("auth.flow");
         };
 
         service.getAuthDataHeader = function () {
@@ -330,11 +398,11 @@ solrAdminServices.factory('System',
         service.isJwtCallback = function (hash) {
           var hp = this.decodeHashParams(hash);
           // console.log("Decoded hash as " + JSON.stringify(hp, undefined, 2)); // For debugging callbacks
-          return (hp['access_token'] && hp['token_type'] && hp['state']) || hp['error'];
+          return (hp['access_token'] && hp['token_type'] && hp['state']) || (hp['code'] && hp['state'])|| hp['error'];
         };
-        
+
         service.decodeHashParams = function(hash) {
-          // access_token, token_type, expires_in, state
+          // access_token, token_type, expires_in, state, code
           if (hash == null || hash.length === 0) {
             return {};
           }
diff --git a/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js b/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js
new file mode 100644
index 00000000000..697ee447f4c
--- /dev/null
+++ b/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js
@@ -0,0 +1,24 @@
+/*
+
+ * A JavaScript implementation of the SHA family of hashes - defined in FIPS PUB 180-4, FIPS PUB 202,
+ * and SP 800-185 - as well as the corresponding HMAC implementation as defined in FIPS PUB 198-1.
+ *
+ * Copyright 2008-2023 Brian Turek, 1998-2009 Paul Johnston & Contributors
+ * Distributed under the BSD License
+ * See http://caligatio.github.com/jsSHA/ for more information
+ *
+ * Two ECMAScript polyfill functions carry the following license:
+ *
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
+ * INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+ * MERCHANTABLITY OR NON-INFRINGEMENT.
+ *
+ * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
+
+ */
+
+!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(t="undefined"!=typeof globalThis?globalThis:t||self).jsSHA=r()}(this,function(){"use strict";var n=function(t,r){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(t[n]=r[n])})(t,r)},v="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01 [...]
\ No newline at end of file