From 7ef62536ddc0e50eca00d458eea8bae610375a4b Mon Sep 17 00:00:00 2001 From: Lamine Idjeraoui Date: Tue, 18 Jul 2023 13:33:29 -0500 Subject: [PATCH 1/7] add support of OAuth 2.0/OIDC 'code with PKCE' flow (front-end) --- .../web/js/angular/controllers/login.js | 270 ++++++++++++------ solr/webapp/web/js/angular/services.js | 77 ++++- 2 files changed, 260 insertions(+), 87 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js index b76ec1f4a8a1..c24ad4fb62f7 100644 --- a/solr/webapp/web/js/angular/controllers/login.js +++ b/solr/webapp/web/js/angular/controllers/login.js @@ -60,92 +60,169 @@ 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 { + // for backward compatibility default flow to 'implicit' + var flow = $scope.authData ? $scope.authData['authorization_flow'] : "implicit"; + console.log("Callback: authorization_flow : " +flow); + var isCodePKCE = flow == 'code_pkce'; + if (isCodePKCE) { + // code flow with PKCE + console.log("Callback. Using 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.log(`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 if(true) { + // implicit flow + console.log("Callback. Using 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.log("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.log("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.log("Expected JWT compact id_token param but got " + idTokenArray); + 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 (accessToken.split(".").length !== 3) { + console.log("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.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']]; + } + 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.log(`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 +242,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.log("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.log("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 +297,7 @@ solrAdminApp.controller('LoginController', for (var p in params) { if( params.hasOwnProperty(p) ) { arr.push(p + "=" + encodeURIComponent(params[p])); - } + } } return arr.join("&"); } @@ -204,7 +310,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 1c0b702dbe1f..53bafe715464 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -286,8 +286,74 @@ 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 encoder = new TextEncoder(); + return window.crypto.subtle.digest("SHA-256", encoder.encode(str)); + } + + 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 +371,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 +397,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 {}; } From b184d0fb48d9716370086bcffdd58fb2059cba5d Mon Sep 17 00:00:00 2001 From: Lamine Idjeraoui Date: Wed, 19 Jul 2023 08:08:55 -0500 Subject: [PATCH 2/7] add support of OAuth 2.0/OIDC 'code with PKCE' flow (back-end) use some console.debug/error instead of console.log --- .../web/js/angular/controllers/login.js | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js index c24ad4fb62f7..5af2b25cf19a 100644 --- a/solr/webapp/web/js/angular/controllers/login.js +++ b/solr/webapp/web/js/angular/controllers/login.js @@ -74,7 +74,7 @@ solrAdminApp.controller('LoginController', var isCodePKCE = flow == 'code_pkce'; if (isCodePKCE) { // code flow with PKCE - console.log("Callback. Using code flow with PKCE"); + console.debug("Callback. Using 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 @@ -90,7 +90,7 @@ solrAdminApp.controller('LoginController', "client_id": $scope.authData['client_id'] }; - console.log(`Callback. Got code: ${code} \nCalling token endpoint:: ${tokenEndpoint} `); + 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; @@ -110,9 +110,9 @@ solrAdminApp.controller('LoginController', } }); } - else if(true) { + else { // implicit flow - console.log("Callback. Using implicit flow"); + console.debug("Callback. Using implicit flow"); processTokensResponse(hp['access_token'], hp['id_token'], hp['token_type'], expectedState, hp); } } @@ -122,7 +122,7 @@ solrAdminApp.controller('LoginController', function validateState(state, expectedState) { if (state !== expectedState) { $scope.error = "Problem with auth callback"; - console.log("Expected state param " + expectedState + " but got " + state); + console.error("Expected state param " + expectedState + " but got " + state); errorText += "Invalid values in state parameter. "; return false; } @@ -137,7 +137,7 @@ solrAdminApp.controller('LoginController', tokenType = "bearer"; } else if(tokenType.toLowerCase() !== "bearer") { - console.log("Expected token_type param 'bearer', but got " + tokenType); + 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 @@ -164,15 +164,15 @@ solrAdminApp.controller('LoginController', $location.path(redirectTo).hash(""); } } else { - console.log("Expected JWT compact id_token param but got " + idTokenArray); + console.error("Expected JWT compact id_token param but got " + idTokenArray); errorText += "Invalid values in id_token parameter. "; } } else { - console.log("Callback was missing the id_token parameter, could not validate nonce."); + 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.log("Expected JWT compact access_token param but got " + accessToken); + console.error("Expected JWT compact access_token param but got " + accessToken); errorText += "Invalid values in access_token parameter. "; } if (errorText !== "") { @@ -183,7 +183,7 @@ solrAdminApp.controller('LoginController', // End callback processing } else if (hp['error']) { // The callback had errors - console.log("Error received from idp: " + hp['error']); + 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."; @@ -207,7 +207,7 @@ solrAdminApp.controller('LoginController', sessionStorage.setItem("auth.state", "error"); } else{ - console.log(`Invalid data received from idp: accessToken: ${accessToken}, + 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"; @@ -255,7 +255,7 @@ solrAdminApp.controller('LoginController', var params = {}; if (isCodePKCE) { - console.log("Login with 'Code PKCE' flow"); + console.debug("Login with 'Code PKCE' flow"); var codeVerifier = AuthenticationService.generateCodeVerifier(); var code_challenge = await AuthenticationService.generateCodeChallengeFromVerifier(codeVerifier); var codeChallengeMethod = AuthenticationService.getCodeChallengeMethod(); @@ -273,7 +273,7 @@ solrAdminApp.controller('LoginController', }; } else { - console.log("Login with 'Implicit' flow"); + console.debug("Login with 'Implicit' flow"); params = { "response_type": "id_token token", "client_id": $scope.authData['client_id'], From 170667e53cc4f2ddf87fb7212130512d8ae312a3 Mon Sep 17 00:00:00 2001 From: Lamine Idjeraoui Date: Mon, 21 Aug 2023 14:42:00 -0500 Subject: [PATCH 3/7] Remove redundant debug. Use server side default value of 'authorization_flow' --- solr/webapp/web/js/angular/controllers/login.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/solr/webapp/web/js/angular/controllers/login.js b/solr/webapp/web/js/angular/controllers/login.js index 5af2b25cf19a..0e22100c306d 100644 --- a/solr/webapp/web/js/angular/controllers/login.js +++ b/solr/webapp/web/js/angular/controllers/login.js @@ -68,13 +68,11 @@ solrAdminApp.controller('LoginController', sessionStorage.setItem("auth.state", "error"); } else { - // for backward compatibility default flow to 'implicit' - var flow = $scope.authData ? $scope.authData['authorization_flow'] : "implicit"; + 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 - console.debug("Callback. Using 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 @@ -112,7 +110,6 @@ solrAdminApp.controller('LoginController', } else { // implicit flow - console.debug("Callback. Using implicit flow"); processTokensResponse(hp['access_token'], hp['id_token'], hp['token_type'], expectedState, hp); } } From 27e3a14d401dbd343185afd9e92ea925f53b6981 Mon Sep 17 00:00:00 2001 From: Lamine Idjeraoui Date: Wed, 13 Sep 2023 11:55:53 -0500 Subject: [PATCH 4/7] use jsSHA library for sha256 --- solr/webapp/web/index.html | 1 + solr/webapp/web/js/angular/services.js | 5 +++-- .../webapp/web/libs/jssha-3.3.1-sha256.min.js | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 solr/webapp/web/libs/jssha-3.3.1-sha256.min.js diff --git a/solr/webapp/web/index.html b/solr/webapp/web/index.html index afffe67f7645..35b1d6648607 100644 --- a/solr/webapp/web/index.html +++ b/solr/webapp/web/index.html @@ -70,6 +70,7 @@ + diff --git a/solr/webapp/web/js/angular/services.js b/solr/webapp/web/js/angular/services.js index 53bafe715464..2b870ab53ff1 100644 --- a/solr/webapp/web/js/angular/services.js +++ b/solr/webapp/web/js/angular/services.js @@ -328,8 +328,9 @@ solrAdminServices.factory('System', } function sha256(str) { - const encoder = new TextEncoder(); - return window.crypto.subtle.digest("SHA-256", encoder.encode(str)); + const shaObj = new jsSHA("SHA-256", "TEXT", { encoding: "UTF8" }); + shaObj.update(str); + return shaObj.getHash("UINT8ARRAY"); } function base64urlencode(a) { 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 000000000000..6a5b72ff5a4c --- /dev/null +++ b/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js @@ -0,0 +1,21 @@ +/** + * 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="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="ARRAYBUFFER not supported by this environment",e="UINT8ARRAY not supported by this environment";function o(t,r,n,e){for(var o,i,s=r||[0],u=(n=n||0)>>>3,h=-1===e?3:0,a=0;a>>2,s.length<=o&&s.push(0),s[o]|=t[a]<<8*(h+e*(i%4));return{value:s,binLen:8*t.length+n}}function i(t,A,g){switch(A){case"UTF8":case"UTF16BE":case"UTF16LE":break;default:throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE")}switch(t){case"HEX":return function(t,r,n){var e,o,i,s=t,t=r,r=n,u=g;if(0!=s.length%2)throw new Error("String of HEX type must be in byte increments");for(var h=t||[0],a=(r=r||0)>>>3,f=-1===u?3:0,c=0;c>>1)+a)>>>2;h.length<=o;)h.push(0);h[o]|=e<<8*(f+u*(i%4))}return{value:h,binLen:4*s.length+r}};case"TEXT":return function(t,r,n){var e,o,i,s,u,h,a,f,c=t,t=A,p=0,l=g,w=0,d=r||[0],v=(p=n||0)>>>3;if("UTF8"===t)for(a=-1===l?3:0,i=0;i>>6),o.push(128|63&e)):e<55296||57344<=e?o.push(224|e>>>12,128|e>>>6&63,128|63&e):(e=65536+((1023&e)<<10|1023&c.charCodeAt(i+=1)),o.push(240|e>>>18,128|e>>>12&63,128|e>>>6&63,128|63&e)),s=0;s>>2;d.length<=u;)d.push(0);d[u]|=o[s]<<8*(a+l*(h%4)),w+=1}else for(a=-1===l?2:0,f="UTF16LE"===t&&1!==l||"UTF16LE"!==t&&1===l,i=0;i>>8),u=(h=w+v)>>>2;d.length<=u;)d.push(0);d[u]|=e<<8*(a+l*(h%4)),w+=2}return{value:d,binLen:8*w+p}};case"B64":return function(t,r,n){var e,o,i,s,u,h,a=t,t=0,f=g,c=0,p=r||[0],l=(t=n||0)>>>3,w=-1===f?3:0,r=a.indexOf("=");if(-1===a.search(/^[a-zA-Z0-9=+/]+$/))throw new Error("Invalid character in base-64 string");if(a=a.replace(/=/g,""),-1!==r&&r Date: Wed, 13 Sep 2023 21:22:12 +0200 Subject: [PATCH 6/7] SOLR-16896: Add notice for the hash js lib --- NOTICE.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NOTICE.txt b/NOTICE.txt index 396db11e6cbd..0fa2229e28a3 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 == From b6ecde84664bfde80a049651f9a7d5cf8fa38e37 Mon Sep 17 00:00:00 2001 From: Lamine Idjeraoui Date: Wed, 13 Sep 2023 15:24:24 -0500 Subject: [PATCH 7/7] fix precommit error (javadoc-style license header [Apache License Version 2.0]) --- solr/webapp/web/libs/jssha-3.3.1-sha256.min.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index 6a5b72ff5a4c..697ee447f4ca 100644 --- a/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js +++ b/solr/webapp/web/libs/jssha-3.3.1-sha256.min.js @@ -1,4 +1,5 @@ -/** +/* + * 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. * @@ -17,5 +18,7 @@ * 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="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="ARRAYBUFFER not supported by this environment",e="UINT8ARRAY not supported by this environment";function o(t,r,n,e){for(var o,i,s=r||[0],u=(n=n||0)>>>3,h=-1===e?3:0,a=0;a>>2,s.length<=o&&s.push(0),s[o]|=t[a]<<8*(h+e*(i%4));return{value:s,binLen:8*t.length+n}}function i(t,A,g){switch(A){case"UTF8":case"UTF16BE":case"UTF16LE":break;default:throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE")}switch(t){case"HEX":return function(t,r,n){var e,o,i,s=t,t=r,r=n,u=g;if(0!=s.length%2)throw new Error("String of HEX type must be in byte increments");for(var h=t||[0],a=(r=r||0)>>>3,f=-1===u?3:0,c=0;c>>1)+a)>>>2;h.length<=o;)h.push(0);h[o]|=e<<8*(f+u*(i%4))}return{value:h,binLen:4*s.length+r}};case"TEXT":return function(t,r,n){var e,o,i,s,u,h,a,f,c=t,t=A,p=0,l=g,w=0,d=r||[0],v=(p=n||0)>>>3;if("UTF8"===t)for(a=-1===l?3:0,i=0;i>>6),o.push(128|63&e)):e<55296||57344<=e?o.push(224|e>>>12,128|e>>>6&63,128|63&e):(e=65536+((1023&e)<<10|1023&c.charCodeAt(i+=1)),o.push(240|e>>>18,128|e>>>12&63,128|e>>>6&63,128|63&e)),s=0;s>>2;d.length<=u;)d.push(0);d[u]|=o[s]<<8*(a+l*(h%4)),w+=1}else for(a=-1===l?2:0,f="UTF16LE"===t&&1!==l||"UTF16LE"!==t&&1===l,i=0;i>>8),u=(h=w+v)>>>2;d.length<=u;)d.push(0);d[u]|=e<<8*(a+l*(h%4)),w+=2}return{value:d,binLen:8*w+p}};case"B64":return function(t,r,n){var e,o,i,s,u,h,a=t,t=0,f=g,c=0,p=r||[0],l=(t=n||0)>>>3,w=-1===f?3:0,r=a.indexOf("=");if(-1===a.search(/^[a-zA-Z0-9=+/]+$/))throw new Error("Invalid character in base-64 string");if(a=a.replace(/=/g,""),-1!==r&&r