diff --git a/docker-compose/mock-relying-party-portal-fapi2-docker-compose.yml b/docker-compose/mock-relying-party-portal-fapi2-docker-compose.yml index 608dd72d..9bec57bc 100644 --- a/docker-compose/mock-relying-party-portal-fapi2-docker-compose.yml +++ b/docker-compose/mock-relying-party-portal-fapi2-docker-compose.yml @@ -45,5 +45,6 @@ services: - PAR_CALLBACK_NAME=get_requestUri - PAR_CALLBACK_TIMEOUT=5000 - DPOP_CALLBACK_NAME=get_dpop_jkt + - CODE_CHALLENGE=get_code_challenge volumes: - ./nginx.conf:/etc/nginx/nginx.conf \ No newline at end of file diff --git a/mock-relying-party-service/app.js b/mock-relying-party-service/app.js index 2f8460ff..c0c8f3d1 100644 --- a/mock-relying-party-service/app.js +++ b/mock-relying-party-service/app.js @@ -39,8 +39,16 @@ app.get("/dpopJKT", rateLimiter, async (req, res) => { app.get("/requestUri/:clientId", async (req, res) => { try { + const { ui_locales, state, dpop_jkt, code_challenge, code_challenge_method } = req.query; res.send( - await post_GetRequestUri(req.params.clientId, req.query.ui_locales, req.query.state, req.query.dpop_jkt), + await post_GetRequestUri( + req.params.clientId, + ui_locales, + state, + dpop_jkt, + code_challenge, + code_challenge_method + ) ); } catch (error) { console.log(error); diff --git a/mock-relying-party-service/esignetService.js b/mock-relying-party-service/esignetService.js index e3f55999..39e377e9 100644 --- a/mock-relying-party-service/esignetService.js +++ b/mock-relying-party-service/esignetService.js @@ -34,8 +34,9 @@ const post_GetToken = async ({ client_id, redirect_uri, grant_type, + code_verifier, }) => { - let request = new URLSearchParams({ + const request = new URLSearchParams({ code: code, client_id: client_id, redirect_uri: redirect_uri, @@ -43,6 +44,12 @@ const post_GetToken = async ({ client_assertion_type: CLIENT_ASSERTION_TYPE, client_assertion: await generateSignedJwt(client_id, ESIGNET_AUD_URL), }); + + // Add code_verifier if provided + if (code_verifier) { + request.append("code_verifier", code_verifier); + } + const endpoint = baseUrl + getTokenEndPoint; const dpopHeaders = await buildDpopHeaders({ clientId: client_id, @@ -85,7 +92,7 @@ const post_GetToken = async ({ * @param {string} clientId clientId * @returns requestUri */ -const post_GetRequestUri = async (clientId, uiLocales, state, dpop_jkt) => { +const post_GetRequestUri = async (clientId, uiLocales, state, dpop_jkt, code_challenge, code_challenge_method) => { const clientAssertion = await generateSignedJwt( clientId, ESIGNET_PAR_AUD_URL @@ -108,6 +115,10 @@ const post_GetRequestUri = async (clientId, uiLocales, state, dpop_jkt) => { if (dpop_jkt) { params.append("dpop_jkt", dpop_jkt); } + if (code_challenge && code_challenge_method) { + params.append("code_challenge", code_challenge); + params.append("code_challenge_method", code_challenge_method); + } const endpoint = clientDetails.parEndpoint; const dpopHeaders = await buildDpopHeaders({ clientId, diff --git a/mock-relying-party-ui/Dockerfile b/mock-relying-party-ui/Dockerfile index 9d946fa6..3586c5ef 100644 --- a/mock-relying-party-ui/Dockerfile +++ b/mock-relying-party-ui/Dockerfile @@ -22,6 +22,7 @@ ARG fallback_lang ARG par_callback_name ARG par_callback_timeout ARG dpop_callback_name +ARG code_challenge ENV ESIGNET_UI_BASE_URL=$esignet_ui_base_url ENV MOCK_RELYING_PARTY_SERVER_URL=$mock_relying_party_server_url @@ -44,6 +45,7 @@ ENV FALLBACK_LANG=$fallback_lang ENV PAR_CALLBACK_NAME=$par_callback_name ENV PAR_CALLBACK_TIMEOUT=$par_callback_timeout ENV DPOP_CALLBACK_NAME=$dpop_callback_name +ENV CODE_CHALLENGE=$code_challenge # Set the environment variable as a placeholder for PUBLIC_URL ENV PUBLIC_URL=_PUBLIC_URL_ @@ -126,6 +128,7 @@ RUN echo "ESIGNET_UI_BASE_URL=$ESIGNET_UI_BASE_URL" >> ${work_dir}/env.env \ && echo "PAR_CALLBACK_NAME=$PAR_CALLBACK_NAME" >> ${work_dir}/env.env \ && echo "PAR_CALLBACK_TIMEOUT=$PAR_CALLBACK_TIMEOUT" >> ${work_dir}/env.env \ && echo "DPOP_CALLBACK_NAME=$DPOP_CALLBACK_NAME" >> ${work_dir}/env.env \ + && echo "CODE_CHALLENGE=$CODE_CHALLENGE" >> ${work_dir}/env.env \ && chmod +x configure_start.sh \ && chown ${container_user}:${container_user} configure_start.sh \ && chown -R ${container_user}:${container_user} /home/${container_user} ${work_dir} diff --git a/mock-relying-party-ui/README.md b/mock-relying-party-ui/README.md index 749b1235..4558bdd7 100644 --- a/mock-relying-party-ui/README.md +++ b/mock-relying-party-ui/README.md @@ -46,14 +46,17 @@ The application runs on PORT=5000 by default. (Example: par_callback_timeout: 5000) - DPOP_CALLBACK_NAME: **Feature flag** to enable DPoP (Demonstration of Proof-of-Possession) flow Required value: `get_dpop_jkt` (hardcoded function name - not configurable) + - CODE_CHALLENGE: **Feature flag** to enable PKCE (Proof Key for Code Exchange) flow + Required value: `get_code_challenge` (hardcoded function name - not configurable) + When enabled, the PKCE method is automatically fetched from the authorization server's `.well-known/openid-configuration` endpoint - > **Important:** PAR_CALLBACK_NAME and DPOP_CALLBACK_NAME act as feature toggles. The values correspond to hardcoded function names in the codebase and are not configurable. Include these variables to enable the respective flows, or omit them to disable the functionality. + > **Important:** PAR_CALLBACK_NAME, DPOP_CALLBACK_NAME, and CODE_CHALLENGE act as feature toggles. The values correspond to hardcoded function names in the codebase and are not configurable. Include these variables to enable the respective flows, or omit them to disable the functionality. - Build and run Docker for a service: ``` $ docker build -t : . - $ docker run -it -d -p 5000:5000 -e ESIGNET_UI_BASE_URL='http://localhost:3000' -e MOCK_RELYING_PARTY_BASE_URL=http://localhost:8888 -e REDIRECT_URI=http://localhost:5000/userprofile -e CLIENT_ID=healthservices -e ACRS="mosip:esignet:acr:static-code" -e MAX_AGE=21 -e DISPLAY=page -e PROMPT=consent -e GRANT_TYPE=authorization_code -e SIGN_IN_BUTTON_PLUGIN_URL='http://127.0.0.1:5500/dist/iife/index.js' -e SCOPE_USER_PROFILE='openid%20profile%20resident-service' -e PAR_CALLBACK_NAME='get_requestUri' -e DPOP_CALLBACK_NAME='get_dpop_jkt' -e : + $ docker run -it -d -p 5000:5000 -e ESIGNET_UI_BASE_URL='http://localhost:3000' -e MOCK_RELYING_PARTY_BASE_URL=http://localhost:8888 -e REDIRECT_URI=http://localhost:5000/userprofile -e CLIENT_ID=healthservices -e ACRS="mosip:esignet:acr:static-code" -e MAX_AGE=21 -e DISPLAY=page -e PROMPT=consent -e GRANT_TYPE=authorization_code -e SIGN_IN_BUTTON_PLUGIN_URL='http://127.0.0.1:5500/dist/iife/index.js' -e SCOPE_USER_PROFILE='openid%20profile%20resident-service' -e PAR_CALLBACK_NAME='get_requestUri' -e DPOP_CALLBACK_NAME='get_dpop_jkt' -e CODE_CHALLENGE='get_code_challenge' : ``` To host the mock relying party UI on a context path: diff --git a/mock-relying-party-ui/public/env-config.js b/mock-relying-party-ui/public/env-config.js index e26e67a6..63092ff5 100644 --- a/mock-relying-party-ui/public/env-config.js +++ b/mock-relying-party-ui/public/env-config.js @@ -21,4 +21,5 @@ window._env_ = { PAR_CALLBACK_NAME: "get_requestUri", PAR_CALLBACK_TIMEOUT: 5000, DPOP_CALLBACK_NAME: "get_dpop_jkt", -}; + CODE_CHALLENGE: "get_code_challenge" + }; diff --git a/mock-relying-party-ui/public/locales/ar.json b/mock-relying-party-ui/public/locales/ar.json index b45a27ce..65a1aee3 100644 --- a/mock-relying-party-ui/public/locales/ar.json +++ b/mock-relying-party-ui/public/locales/ar.json @@ -247,6 +247,7 @@ "request_uri_timeout": "انتهت مهلة الطلب أثناء جلب رابط URI الخاص بالطلب. يُرجى المحاولة لاحقًا.", "web_socket_fail": "لم يتم إكمال عملية التحقق من eKYC. يُرجى المحاولة مرة أخرى.", "dpop_failed": "فشل التحقق من DPoP. يُرجى المحاولة مرة أخرى.", - "invalid_dpop_proof": "لم نتمكن من إتمام المصادقة. يرجى المحاولة مرة أخرى" + "invalid_dpop_proof": "لم نتمكن من إتمام المصادقة. يرجى المحاولة مرة أخرى", + "code_challenge_failed": "فشل في إنشاء تحدي الشفرة. يرجى المحاولة مرة أخرى." } } diff --git a/mock-relying-party-ui/public/locales/en.json b/mock-relying-party-ui/public/locales/en.json index 8db4d525..458e624f 100644 --- a/mock-relying-party-ui/public/locales/en.json +++ b/mock-relying-party-ui/public/locales/en.json @@ -248,6 +248,7 @@ "request_uri_timeout": "Request timed out while fetching Request URI. Please try again later.", "web_socket_fail": "eKYC verification was not completed. Please try again.", "dpop_failed": "DPoP verification failed. Please try again.", - "invalid_dpop_proof": "Authentication could not be completed. Please try again" + "invalid_dpop_proof": "Authentication could not be completed. Please try again", + "code_challenge_failed": "Failed to generate code challenge. Please try again." } } diff --git a/mock-relying-party-ui/public/locales/hi.json b/mock-relying-party-ui/public/locales/hi.json index 1c0cccef..c12396da 100644 --- a/mock-relying-party-ui/public/locales/hi.json +++ b/mock-relying-party-ui/public/locales/hi.json @@ -246,6 +246,7 @@ "request_uri_timeout": "अनुरोध URI प्राप्त करते समय अनुरोध का समय समाप्त हो गया। कृपया बाद में पुनः प्रयास करें।", "web_socket_fail": "eKYC सत्यापन पूरा नहीं हुआ। कृपया पुनः प्रयास करें।", "dpop_failed": "DPoP सत्यापन विफल हुआ. कृपया पुनः प्रयास करें।", - "invalid_dpop_proof": "प्रमाणीकरण पूरा नहीं हो सका। कृपया पुनः प्रयास करें" + "invalid_dpop_proof": "प्रमाणीकरण पूरा नहीं हो सका। कृपया पुनः प्रयास करें", + "code_challenge_failed": "कोड चुनौती उत्पन्न करने में विफल। कृपया पुनः प्रयास करें।" } } diff --git a/mock-relying-party-ui/public/locales/km.json b/mock-relying-party-ui/public/locales/km.json index 5476009e..ce384663 100644 --- a/mock-relying-party-ui/public/locales/km.json +++ b/mock-relying-party-ui/public/locales/km.json @@ -248,6 +248,7 @@ "request_uri_timeout": "សំណើអស់ពេលពេលទៅយកសំណើ URI ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ។", "web_socket_fail": "ការផ្ទៀងផ្ទាត់ eKYC មិនត្រូវបានបញ្ចប់ទេ។ សូមព្យាយាមម្តងទៀត។", "dpop_failed": "ការផ្ទៀងផ្ទាត់ DPoP បានបរាជ័យ។ សូមព្យាយាមម្តងទៀត។", - "invalid_dpop_proof": "ការផ្ទៀងផ្ទាត់មិនអាចបញ្ចប់បាន។ សូមព្យាយាមម្ដងទៀត" + "invalid_dpop_proof": "ការផ្ទៀងផ្ទាត់មិនអាចបញ្ចប់បាន។ សូមព្យាយាមម្ដងទៀត", + "code_challenge_failed": "ការបង្កើតការប្រឈមកូដបានបរាជ័យ។ សូមព្យាយាមម្ដងទៀត។" } } diff --git a/mock-relying-party-ui/public/locales/kn.json b/mock-relying-party-ui/public/locales/kn.json index ff4c8385..90c949eb 100644 --- a/mock-relying-party-ui/public/locales/kn.json +++ b/mock-relying-party-ui/public/locales/kn.json @@ -246,6 +246,7 @@ "request_uri_timeout": "ವಿನಂತಿ URI ಅನ್ನು ಪಡೆಯುವಾಗ ವಿನಂತಿಯ ಸಮಯ ಮೀರಿದೆ. ದಯವಿಟ್ಟು ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", "web_socket_fail": "eKYC ಪರಿಶೀಲನೆ ಪೂರ್ಣಗೊಂಡಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", "dpop_failed": "DPoP ಪರಿಶೀಲನೆ ವಿಫಲವಾಗಿದೆ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", - "invalid_dpop_proof": "ಪ್ರಾಮಾಣೀಕರಣವನ್ನು ಪೂರ್ಣಗೊಳಿಸಲಾಗಲಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ" + "invalid_dpop_proof": "ಪ್ರಾಮಾಣೀಕರಣವನ್ನು ಪೂರ್ಣಗೊಳಿಸಲಾಗಲಿಲ್ಲ. ದಯವಿಟ್ಟು ಮತ್ತೊಮ್ಮೆ ಪ್ರಯತ್ನಿಸಿ", + "code_challenge_failed": "ಕೋಡ್ ಚಾಲೆಂಜ್ ರಚಿಸಲು ವಿಫಲವಾಗಿದೆ. ದಯವಿಟ್ಟು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." } } diff --git a/mock-relying-party-ui/public/locales/ta.json b/mock-relying-party-ui/public/locales/ta.json index 3108a28e..96102bb2 100644 --- a/mock-relying-party-ui/public/locales/ta.json +++ b/mock-relying-party-ui/public/locales/ta.json @@ -246,6 +246,7 @@ "request_uri_timeout": "கோரிக்கை URI-ஐப் பெறும்போது கோரிக்கை நேரம் முடிந்தது. தயவுசெய்து பின்னர் மீண்டும் முயற்சிக்கவும்.", "web_socket_fail": "eKYC சரிபார்ப்பு முடிக்கப்படவில்லை. மீண்டும் முயற்சிக்கவும்.", "dpop_failed": "DPoP சரிபார்ப்பு தோல்வியடைந்தது. மீண்டும் முயற்சிக்கவும்.", - "invalid_dpop_proof": "அங்கீகாரம் முடிக்க முடியவில்லை. மீண்டும் முயற்சிக்கவும்" + "invalid_dpop_proof": "அங்கீகாரம் முடிக்க முடியவில்லை. மீண்டும் முயற்சிக்கவும்", + "code_challenge_failed": "கோட் சவால் உருவாக்க முடியவில்லை. தயவுசெய்து மீண்டும் முயற்சிக்கவும்." } } diff --git a/mock-relying-party-ui/src/components/Login.js b/mock-relying-party-ui/src/components/Login.js index 6110a9f2..2c58a8af 100644 --- a/mock-relying-party-ui/src/components/Login.js +++ b/mock-relying-party-ui/src/components/Login.js @@ -51,7 +51,8 @@ export default function Login({ i18nKeyPrefix = "login" }) { claims: JSON.parse(decodeURIComponent(clientDetails.userProfileClaims)), par_callback: relyingPartyService[clientDetails.par_callback_name], par_callback_timeout: clientDetails.par_callback_timeout, - dpop_callback: relyingPartyService[clientDetails.dpop_callback_name] + dpop_callback: relyingPartyService[clientDetails.dpop_callback_name], + code_challenge: relyingPartyService[clientDetails.code_challenge], }; window.SignInWithEsignetButton?.init({ diff --git a/mock-relying-party-ui/src/components/Sidenav.js b/mock-relying-party-ui/src/components/Sidenav.js index 9b4a0f16..879a2e8a 100644 --- a/mock-relying-party-ui/src/components/Sidenav.js +++ b/mock-relying-party-ui/src/components/Sidenav.js @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Link, useNavigate, useSearchParams } from "react-router-dom"; import clientDetails from "../constants/clientDetails"; import { LoadingStates as states } from "../constants/states"; @@ -29,6 +29,7 @@ export default function Sidenav({ const [emailAddress, setEmailAddress] = useState(null); const [showMenu, setShowMenu] = useState(false); const navigate = useNavigate(); + const hasFetchedRef = useRef(false); function getAllKeys(input) { if (Array.isArray(input)) { @@ -80,6 +81,10 @@ export default function Sidenav({ useEffect(() => { const getSearchParams = async () => { + // Prevent duplicate API calls during React.StrictMode double mounting + if (hasFetchedRef.current) return; + hasFetchedRef.current = true; + let authCode = searchParams.get("code"); let errorCode = searchParams.get("error"); let error_desc = searchParams.get("error_description"); diff --git a/mock-relying-party-ui/src/constants/clientDetails.js b/mock-relying-party-ui/src/constants/clientDetails.js index a5f10dbe..151736b1 100644 --- a/mock-relying-party-ui/src/constants/clientDetails.js +++ b/mock-relying-party-ui/src/constants/clientDetails.js @@ -59,6 +59,7 @@ const par_callback_timeout = checkEmptyNullValue( 5000 ); const dpop_callback_name = window._env_.DPOP_CALLBACK_NAME; +const code_challenge = window._env_.CODE_CHALLENGE; const claims = { userinfo: { given_name: { @@ -108,6 +109,7 @@ const clientDetails = { par_callback_name: par_callback_name, par_callback_timeout: par_callback_timeout, dpop_callback_name: dpop_callback_name, + code_challenge: code_challenge, }; export default clientDetails; diff --git a/mock-relying-party-ui/src/services/relyingPartyService.js b/mock-relying-party-ui/src/services/relyingPartyService.js index 9bb31b04..320cc217 100644 --- a/mock-relying-party-ui/src/services/relyingPartyService.js +++ b/mock-relying-party-ui/src/services/relyingPartyService.js @@ -6,6 +6,81 @@ import { GET_DPOP_JKT, } from "../constants/routes"; +/** + * Base64URL encoding utility for PKCE + * @param {Uint8Array} buffer - Byte array to encode + * @returns {string} - Base64URL encoded string + */ +const base64UrlEncode = (buffer) => { + let binary = ''; + for (let i = 0; i < buffer.byteLength; i++) { + binary += String.fromCharCode(buffer[i]); + } + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +}; + +/** + * Generates a cryptographically secure code verifier + * @returns {string} - Base64URL encoded code verifier + */ +const generateCodeVerifier = () => { + return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32))); +}; + +/** + * Fetches supported PKCE methods from auth server's .well-known + * @returns {Promise} - Supported code challenge method (defaults to 'S256') + */ +const get_code_challenge_method = async () => { + try { + const baseUrl = window._env_.ESIGNET_UI_BASE_URL; + const response = await axios.get(`${baseUrl}/.well-known/openid-configuration`); + const supportedMethods = response.data?.code_challenge_methods_supported || []; + + // Default to S256 + if (!supportedMethods || supportedMethods.length === 0) return 'S256'; + + // If method contains S256, use S256; otherwise fallback to first supported method. + return supportedMethods.includes('S256') ? 'S256' : supportedMethods[0]; + } catch (error) { + console.warn('Failed to fetch PKCE methods, defaulting to S256:', error); + return 'S256'; + } +}; + +/** + * Generates PKCE code challenge and stores verifier in sessionStorage + * @param {string} clientId - Registered client ID + * @param {string} state - Unique state value + * @returns {Promise} - Object with code_challenge and code_challenge_method + */ +const get_code_challenge = async (clientId, state) => { + const method = await get_code_challenge_method(); + + if (!method) { + console.warn('PKCE disabled: no supported method found'); + return null; + } + + const codeVerifier = generateCodeVerifier(); + let codeChallenge; + + if (method === 'S256') { + const encoder = new TextEncoder(); + const data = encoder.encode(codeVerifier); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + codeChallenge = base64UrlEncode(new Uint8Array(hashBuffer)); + } else { + codeChallenge = codeVerifier; + } + + sessionStorage.setItem(`pkce_${clientId}_${state}`, codeVerifier); + return { + code_challenge: codeChallenge, + code_challenge_method: method + }; +}; + /** * Fetches the DPoP JWK thumbprint (JKT) from the relying party server. * @param {string} clientId - Registered client ID @@ -35,15 +110,19 @@ const get_dpop_jkt = async (clientId, state) => { * @param {string} clientId - Registered client ID * @param {string} state - Unique state value for the authorization request * @param {string} ui_locales - Locale/language preference + * @param {string} dpop_jkt - DPoP JWK thumbprint (optional) + * @param {string} code_challenge - PKCE code challenge (optional) + * @param {string} code_challenge_method - PKCE method (optional) * @returns {Promise} - Request URI (URN format) */ -const get_requestUri = async (clientId, state, ui_locales, dpop_jkt) => { +const get_requestUri = async (clientId, state, ui_locales, dpop_jkt, code_challenge, code_challenge_method) => { try { - const params = new URLSearchParams({ - state, - ui_locales, - dpop_jkt - }); + const params = new URLSearchParams({ state, ui_locales }); + if (dpop_jkt) params.append("dpop_jkt", dpop_jkt); + if (code_challenge && code_challenge_method) { + params.append("code_challenge", code_challenge); + params.append("code_challenge_method", code_challenge_method); + } const endpoint = `${BASE_URL}${GET_REQUEST_URI}/${clientId}?${params.toString()}`; const response = await axios.get(endpoint, { headers: { @@ -61,6 +140,7 @@ const get_requestUri = async (clientId, state, ui_locales, dpop_jkt) => { * Typically called after receiving the auth code from the authorization server. * * @param {string} code - Authorization code received after user consent + * @param {string} state - State value from authorization request * @param {string} client_id - Registered client ID * @param {string} redirect_uri - Redirect URI used during authorization * @param {string} grant_type - OAuth 2.0 grant type (usually "authorization_code") @@ -73,19 +153,31 @@ const post_fetchUserInfo = async ( redirect_uri, grant_type ) => { - let request = { - code: code, - state: state, - client_id: client_id, - redirect_uri: redirect_uri, - grant_type: grant_type, + const request = { + code, + state, + client_id, + redirect_uri, + grant_type, }; + + // Retrieve and include code_verifier if it exists in sessionStorage (PKCE enabled) + const codeVerifier = sessionStorage.getItem(`pkce_${client_id}_${state}`); + if (codeVerifier) { + request.code_verifier = codeVerifier; + } + const endpoint = BASE_URL + GET_USER_INFO; const response = await axios.post(endpoint, request, { headers: { "Content-Type": "application/json", }, }); + + if (codeVerifier) { + sessionStorage.removeItem(`pkce_${client_id}_${state}`); + } + return response.data; }; @@ -187,6 +279,8 @@ const relyingPartyService = { get_nextAppointment, get_requestUri, get_dpop_jkt, + get_code_challenge, + get_code_challenge_method }; export default relyingPartyService;