From 374a1a87b61f44cc2168212c38e6315dfeac5ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 30 May 2026 14:19:51 +0300 Subject: [PATCH] =?UTF-8?q?research:=20SSO-=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=20=D0=B8=D0=B7=20payg-report=20(Application.cfc,=20oauth?= =?UTF-8?q?2.cfc,=20jwt.cfc)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- research/payg-sso/Application.cfc | 522 ++++++++++++++++++++++++++++++ research/payg-sso/lib_jwt.cfc | 262 +++++++++++++++ research/payg-sso/lib_oauth2.cfc | 193 +++++++++++ 3 files changed, 977 insertions(+) create mode 100644 research/payg-sso/Application.cfc create mode 100644 research/payg-sso/lib_jwt.cfc create mode 100644 research/payg-sso/lib_oauth2.cfc diff --git a/research/payg-sso/Application.cfc b/research/payg-sso/Application.cfc new file mode 100644 index 0000000..d3f85af --- /dev/null +++ b/research/payg-sso/Application.cfc @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + DE( "https://" ), + DE( "http://" ) + ) & + cgi.http_host & + reReplace( + getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$", + "", + "one" + ) + ) /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + var requestData = GetHttpRequestData(); + var headers = requestData.headers; + + if (structKeyExists(headers,"authorization")) { // аутентификация для клиента ЛК + var authorizationHeader = headers.authorization; + if (authorizationHeader.startsWith("Bearer ")) { + var bearerToken = Mid(authorizationHeader, 8); + writeDump(bearerToken); + //var token=toString(binaryDecode(bearerToken, "base64")); + //writeDump(token); + //jwt = new lib.jwt(this.client_secret); + //lock scope="application" type="readonly" timeout="1" { + // var token_data = (jwt.decode(token, application.idpCertificate,"RS256")); + //} + var result=""; + //try { + var iamService = new http(method = "GET", charset = "utf-8", url = #this.iam_url#, timeout="5"); + iamService.addParam(type = "HEADER", name = "Accept", value = "application/json"); + iamService.addParam(type = "HEADER", name = "Authorization", value = "#authorizationHeader#"); //passthrough + //writedump(this.iamServiceUrl);abort; + var resp = iamService.send(); + //if (resp.status_code NEQ 200) throw("IDP response not OK"); + //writedump(resp);abort; + var prefix = resp.getPrefix(); + if (prefix.status_code NEQ 200) { + //writedump(this.iamServiceUrl); + //writedump(resp); + var iamStatusCode = (isValid("integer", prefix.status_code)) ? val(prefix.status_code) : 500; + throw(message="IAM error", detail="URL: #this.iam_url# Status Code: #prefix.status_code#"); + //abort; + //throw("IDP response not OK"); + } + result = prefix.filecontent; + var idpUserData=deserializeJson(result); + //request.auth.wz=idpUserData.userInfo.ClientId;//GUID! + + + request.auth.login = idpUserData.userInfo.login; + //request.usrUid=idpUserData.userInfo.contactId;//userId; //GUID! + //writedump(idpUserData);abort; + //} catch (e) { + /*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository + var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';} + */ + //writedump(resp); + //return representationOf( {"exception"=e} ); + //rethrow(e); + //} + } + } else { + //точно мы так не словим дедлок ВПР + lock scope="session" type="readonly" timeout="3" { + if (structKeyExists(session, "auth")) { + request.auth = structCopy(session.auth); + } + } + if (structKeyExists(request, "auth") AND Now() < request.auth.exp) { + + if (dateDiff("s", request.auth.iat, Now()) > request.auth.refresh_expires_in/1.5 AND CGI.REQUEST_METHOD EQ "GET") { + // доля времени, после которой происходит рефреш, прибита гвоздями + // По всей видимости, рефреш будет ломать обращения POSТ + // В связи с чем предлагается дождаться GET + // Приклеивать query_string как-то не пришлось, браузер сам справляется + + // refresh + var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri); + var resp = idp.refreshAccessTokenRequest(request.auth.refresh_token); + + lock scope="application" type="readonly" timeout="1" { + idpCertificate = application.idpCertificate; + } + + var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок + + lock scope="session" type="exclusive" timeout="1" { + session.auth = auth; + } + request.auth = auth; + writeDump(auth); + } + } else { + // сессии SSO нет + // или токен протух + request.auth_state = createGUID().toString(); + lock scope="session" type="exclusive" timeout="1" { + structDelete(session, "auth"); // зачистили сессию от данных SSO + session.auth_state = request.auth_state; + } + + var idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri); + /* + часто появляется ошибка: после неудачного запроса к IDP + в адресной строке браузера остается все, с чем его редиректил IDP, + а повторный запрос с этим контентом уже не работает. Получаем 400 Bad Request - + может быть, KeyCloak так защищается от циклических редиректов + Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно) + */ + //writeOutput(getTickCount()-request.startTickCount); + if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение, + + try { + //writeOutput("idp.makeAccessTokenRequest"); + //writeOutput(getTickCount()-request.startTickCount); + var resp = idp.makeAccessTokenRequest(url.code); + + lock scope="application" type="readonly" timeout="1" { + idpCertificate = application.idpCertificate; + } + + var auth = parseIdpresponse(resp, idpCertificate, this.client_secret); //все эти сложности из-за опасений насчет многопоточности и блокировок + + lock scope="session" type="exclusive" timeout="1" { + session.auth = auth; + } + //request.auth = auth; + location(this.redirect_uri, false); + } catch (e) {echo('
****************** #e.message# : #e.detail# **************');} + + } else { //отправляем браузер к IDP + var strURL =idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":request.auth_state,"allow_signup":false}); + echo('self-made Auth link (idp) #strURL#

'); + location(strUrl,false); + } + + } + + } + + //request.logout_url = ""; + if (structKeyExists(request, "auth") AND structKeyExists(request.auth, "id_token")) { + request.logout_url = "#this.logout_endpoint#?id_token_hint=#request.auth.id_token#&post_logout_redirect_uri=#this.redirect_uri#?logout"; + // внимание, здесь подразумевается, что redirect_uri не содержит query_string + // после возвращения от IDP нам нужно будет зачистить сессию SSO, сделаем это по слову logout + } + + request.auth.wz='WZ01348'; //******************* +
+ + + + + + + + + + + ---> + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + select value as stand from config + where name='STAND' + + + + + + + + + + + + + + + + + + + select 1; + + + + + + + + + + + + + + + + + + + + select 2; + + + + + + + + + + + + + + + + + + + //https://keycloak.nubes.ru/admin + //https://keycloak.nubes.ru/realms/SSH_CA/account //smishchuk@mgmt.nubes.ru + //https://stackoverflow.com/questions/28658735/what-are-keycloaks-oauth2-openid-connect-endpoints + //http://https://keycloak.nubes.ru/realms/SSH_CA/.well-known/openid-configuration + + //https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка + //https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт + //только надо его обрамить -----BEGIN CERTIFICATE----- + private function getIdpCertificate(certUrl) { + + var httpService = new http(); + httpService.setMethod( "get" ); + httpService.setCharset( "utf-8" ); + httpService.setUrl(arguments.certUrl); + + var result = httpService.send().getPrefix(); + var status_code = result.ResponseHeader['Status_Code']; + + if ('200' == status_code) { + var content = deserializeJson(result.FileContent); + //мы подразумеваем структуру конкретно KeyCloak + var key = ""; + for (key in content.keys) { + if ("RSA" == key.kty AND "sig" == key.use) { + return "-----BEGIN CERTIFICATE-----" & key.x5c[1] & "-----END CERTIFICATE-----"; + } + } + throw(message="RSA cetificate not found"); + } else { + throw (message="Cannot obtain IDP certificate, request failed", detail="Status_Code #status_code#"); + } + } + + private struct function parseIdpResponse(struct resp, string certificate, string jwt_secret) { + + var auth = structNew("linked"); + var data = deserializeJson(arguments.resp.content); + var jwt = new lib.jwt(arguments.jwt_secret); + + auth.expires_in = data.expires_in; + auth.refresh_expires_in = data.refresh_expires_in; + auth.access_token = data.access_token; + auth.refresh_token = data.refresh_token; + auth.id_token = data.id_token; + auth.token_type = data.token_type; + + //idp_response_content = structCopy(data); //for debug + var token_data = (jwt.decode(data.access_token, arguments.certificate,"RS256")); + + auth.session_state = token_data.session_state; + auth.sid = token_data.sid; + auth.auth_time = token_data.auth_time; + auth.exp = token_data.exp; + auth.iat = token_data.iat; + + auth.login = token_data.preferred_username; + auth.wz = token_data.ClientID; + auth.fullname = token_data.name; + auth.groups = token_data.groups; + //auth.token_data = structCopy(token_data); //for debug + + return (auth); + } + + + + + + + + + + + + +
\ No newline at end of file diff --git a/research/payg-sso/lib_jwt.cfc b/research/payg-sso/lib_jwt.cfc new file mode 100644 index 0000000..e86a066 --- /dev/null +++ b/research/payg-sso/lib_jwt.cfc @@ -0,0 +1,262 @@ +component { + + variables.algorithmMap = { + HS256: 'HmacSHA256', + HS384: 'HmacSHA384', + HS512: 'HmacSHA512', + RS256: 'SHA256withRSA', + RS384: 'SHA384withRSA', + RS512: 'SHA512withRSA', + ES256: 'SHA256withECDSA', + ES384: 'SHA384withECDSA', + ES512: 'SHA512withECDSA' + }; + + public any function init() { + variables.encodingUtils = new encodingUtils(); + variables.jss = createObject( 'java', 'java.security.Signature' ); + variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' ); + return this; + } + + public string function encode( + required struct payload, + required any key, + required string algorithm, + struct headers = { } + ) { + if ( !algorithmMap.keyExists( algorithm ) ) { + throw( + type = 'jwtcfml.InvalidAlgorithm', + message = 'Invalid JWT Algorithm.', + detail = 'The passed in algorithm is not supported.' + ); + } + + var header = { }; + header.append( headers ); + header.append( { + 'typ': 'JWT', + 'alg': algorithm + } ); + + var duplicatedPayload = duplicate( payload ); + for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { + if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) { + duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] ); + } + } + + var stringToSignParts = [ + encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ), + encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) ) + ]; + var stringToSign = stringToSignParts.toList( '.' ); + + return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) ); + } + + public struct function decode( + required string token, + any key, + any algorithms = [ ], + struct claims = { }, + boolean verify = true + ) { + var parts = listToArray( token, '.' ); + + if ( arrayLen( parts ) != 3 ) { + throw( + type = 'jwtcfml.InvalidToken', + message = 'Invalid JWT.', + detail = 'The passed in token does not have three `.` delimited parts.' + ); + } + + algorithms = isArray( algorithms ) ? algorithms : [ algorithms ]; + + var decoded = { + header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ), + payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) ) + }; + + if ( verify ) { + if ( + !algorithms.find( decoded.header.alg ) || + !algorithmMap.keyExists( decoded.header.alg ) + ) { + throw( + type = 'jwtcfml.InvalidAlgorithm', + message = 'Unsupported or invalid algorithm', + detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.' + ); + } + + var stringToSign = parts[ 1 ] & '.' & parts[ 2 ]; + var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] ); + + if ( + !verifySignature( + stringToSign, + key, + signature, + decoded.header.alg + ) + ) { + throw( + type = 'jwtcfml.InvalidSignature', + message = 'Signature is Invalid', + detail = 'The signature of the passed in token is invalid.' + ); + } + + var baseClaims = { + 'exp': true, + 'nbf': true + }; + baseClaims.append( claims ); + verifyClaims( decoded.payload, baseClaims ); + } + + for ( var claim in [ 'iat', 'exp', 'nbf' ] ) { + if ( decoded.payload.keyExists( claim ) ) { + decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] ); + } + } + + return decoded.payload; + } + + public struct function getHeader( required string token ) { + return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) ); + } + + public function parsePEMEncodedKey( required string pemKey ) { + return encodingUtils.parsePEMEncodedKey( pemKey ); + } + + public function parseJWK( required struct jwk ) { + return encodingUtils.parseJWK( jwk ); + } + + private function sign( message, key, algorithm ) { + if ( left( algorithm, 1 ) == 'H' ) { + var sig = binaryDecode( + hmac( + message, + key, + algorithmMap[ algorithm ], + 'utf-8' + ), + 'hex' + ); + } else { + if ( isSimpleValue( key ) ) { + key = encodingUtils.parsePEMEncodedKey( key ); + } else if ( isStruct( key ) ) { + key = encodingUtils.parseJWK( key ); + } + + var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); + jssInstance.initSign( key ); + jssInstance.update( charsetDecode( message, 'utf-8' ) ); + var sig = jssInstance.sign(); + if ( left( algorithm, 1 ) == 'E' ) { + sig = encodingUtils.convertDERtoP1363( sig, algorithm ); + } + } + return sig; + } + + private function verifySignature( message, key, signature, algorithm ) { + if ( left( algorithm, 1 ) == 'H' ) { + var sig = binaryDecode( + hmac( + message, + key, + algorithmMap[ algorithm ], + 'utf-8' + ), + 'hex' + ); + return MessageDigest.isEqual( signature, sig ); + } + + if ( left( algorithm, 1 ) == 'E' ) { + signature = encodingUtils.convertP1363ToDER( signature ); + } + + if ( isSimpleValue( key ) ) { + key = encodingUtils.parsePEMEncodedKey( key ); + } else if ( isStruct( key ) ) { + key = encodingUtils.parseJWK( key ); + } + + var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] ); + jssInstance.initVerify( key ); + jssInstance.update( charsetDecode( message, 'utf-8' ) ); + return jssInstance.verify( signature ); + } + + private function verifyClaims( payload, claims ) { + if ( + structKeyExists( payload, 'exp' ) + && !verifyDateClaim( payload.exp, claims.exp, -1 ) + ) { + throw( + type = 'jwtcfml.ExpiredSignature', + message = 'Token has expired', + detail = 'The passed in token has expired.' + ); + } + + if ( + structKeyExists( payload, 'nbf' ) + && !verifyDateClaim( payload.nbf, claims.nbf, 1 ) + ) { + throw( + type = 'jwtcfml.NotBeforeException', + message = 'Token is not valid', + detail = 'The passed in token has not yet become valid.' + ); + } + + + + if ( structKeyExists( claims, 'iss' ) ) { + if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) { + throw( + type = 'jwtcfml.InvalidIssuer', + message = 'Token has an invalid issuer', + detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.' + ); + } + } + + if ( structKeyExists( claims, 'aud' ) ) { + var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ]; + if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) { + throw( + type = 'jwtcfml.InvalidAudience', + message = 'Token has an invalid audience', + detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.' + ); + } + } + } + + private function verifyDateClaim( payloadDate, claim, failState ) { + var pd = encodingUtils.convertUnixTimestampToDate( payloadDate ); + var cd = claim; + if ( !isBoolean( cd ) || cd ) { + if ( isNumeric( cd ) ) { + cd = encodingUtils.convertUnixTimestampToDate( cd ); + } else if ( !isDate( cd ) ) { + cd = now(); + } + return dateCompare( pd, cd ) != failState; + } + return true; + } + +} diff --git a/research/payg-sso/lib_oauth2.cfc b/research/payg-sso/lib_oauth2.cfc new file mode 100644 index 0000000..41fa80d --- /dev/null +++ b/research/payg-sso/lib_oauth2.cfc @@ -0,0 +1,193 @@ +/** +* @displayname oauth2 +* @output false +* @hint The oauth2 object. +* @author Matt Gifford +* @website https://www.monkehworks.com +* @purpose A ColdFusion Component to manage authentication using the OAuth2 protocol. +**/ +component accessors="true"{ + + property name="client_id" type="string"; + property name="client_secret" type="string"; + property name="authEndpoint" type="string"; + property name="accessTokenEndpoint" type="string"; + property name="redirect_uri" type="string"; + property name="PKCE" type="struct"; + + /** + * I return an initialized oauth2 object instance. + * @client_id The client ID for your application. + * @client_secret The client secret for your application. + * @authEndpoint The URL endpoint that handles the authorisation. + * @accessTokenEndpoint The URL endpoint that handles retrieving the access token. + * @redirect_uri The URL to redirect the user back to following authentication. + **/ + public oauth2 function init( + required string client_id, + required string client_secret, + required string authEndpoint, + required string accessTokenEndpoint, + required string redirect_uri + ){ + setClient_id( arguments.client_id ); + setClient_secret( arguments.client_secret ); + setAuthEndpoint( arguments.authEndpoint ); + setAccessTokenEndpoint( arguments.accessTokenEndpoint ); + setRedirect_uri( arguments.redirect_uri ); + return this; + } + + /** + * I return the URL as a string which we use to redirect the user for authentication. | + * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. + * @parameters A structure containing key / value pairs of data to be included in the URL string. + **/ + public string function buildRedirectToAuthURL( struct parameters={} ) { + var objAuthBuilder = new utils.authStringBuilder( + authEndpoint = getAuthEndpoint(), + client_id = getClient_id(), + redirect_uri = getRedirect_uri() + ); + return objAuthBuilder.withParams( arguments.parameters ).get(); + } + + /** + * I return an instance of the authStringBuilder class. The actual link string is generated by then calling the .get() method on the class object. + * The method will handle the client_id and redirect_uri values for you. Anything else you need to send to the provider in the URL can be sent via the parameters argument. + * @parameters A structure containing key / value pairs of data to be included in the URL string. + **/ + public authStringBuilder function buildRedirectToAuthURLWithBuilder( struct parameters={} ) { + var objAuthBuilder = new utils.authStringBuilder( + authEndpoint = getAuthEndpoint(), + client_id = getClient_id(), + redirect_uri = getRedirect_uri() + ); + return objAuthBuilder.withParams( arguments.parameters ); + } + + /** + * I make the HTTP request to obtain the access token. + * @code The code returned from the authentication request. + * @formfields An optional array of structs for the provider requirements to add new form fields. + * @headers An optional array of structs to add custom headers to the request if required. + **/ + public struct function makeAccessTokenRequest( + required string code, + array formfields = [], + array headers = [] + ){ + var stuResponse = {}; + var httpService = new http(); + httpService.setMethod( "post" ); + httpService.setCharset( "utf-8" ); + httpService.setUrl( getAccessTokenEndpoint() ); + if( arrayLen( arguments.headers ) ){ + for( var item in arguments.headers ){ + httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); + } + } + httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); + httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); + httpService.addParam( type="formfield", name="code", value=arguments.code ); + httpService.addParam( type="formfield", name="redirect_uri", value=getRedirect_uri() ); + httpService.addParam( type="formfield", name="grant_type", value='authorization_code' ); + if( arrayLen( arguments.formfields ) ){ + for( var item in arguments.formfields ){ + httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); + } + } + var result = httpService.send().getPrefix(); + if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { + stuResponse.success = true; + stuResponse.content = result.FileContent; + } else { + stuResponse.success = false; + stuResponse.content = result.Statuscode; + } + //dump(var=formfields,label="form fields"); + return stuResponse; + } + + /** + * I make the HTTP request to refresh the access token. + * @refresh_token The refresh_token returned from the accessTokenRequest request. + **/ + public struct function refreshAccessTokenRequest( + required string refresh_token, + array formfields = [], + array headers = [] + ){ + var stuResponse = {}; + var httpService = new http(); + httpService.setMethod( "post" ); + httpService.setCharset( "utf-8" ); + httpService.setUrl( getAccessTokenEndpoint() ); + httpService.addParam( type="header", name="Content-Type", value="application/x-www-form-urlencoded" ); + if( arrayLen( arguments.headers ) ){ + for( var item in arguments.headers ){ + httpService.addParam( type="header", name=item[ 'name' ], value=item[ 'value' ] ); + } + } + httpService.addParam( type="formfield", name="client_id", value=getClient_id() ); + httpService.addParam( type="formfield", name="client_secret", value=getClient_secret() ); + httpService.addParam( type="formfield", name="refresh_token", value=arguments.refresh_token ); + httpService.addParam( type="formfield", name="grant_type", value="refresh_token" ); + if( arrayLen( arguments.formfields ) ){ + for( var item in arguments.formfields ){ + httpService.addParam( type="formfield", name=item[ 'name' ], value=item[ 'value' ] ); + } + } + var result = httpService.send().getPrefix(); + if( '200' == result.ResponseHeader[ 'Status_Code' ] ) { + stuResponse.success = true; + stuResponse.content = result.FileContent; + } else { + stuResponse.success = false; + stuResponse.content = result.Statuscode; + } + return stuResponse; + } + + /** + * I return a string containing any extra URL parameters to concatenate and pass through when authenticating. + * @argScope A structure containing key / value pairs of data to be included in the URL string. + **/ + public string function buildParamString( struct argScope={} ) { + var strURLParam = '?'; + if( structCount( arguments.argScope ) ) { + var intCount = 1; + for( var key in arguments.argScope ) { + if( listLen( strURLParam ) && intCount > 1 ) { + strURLParam = strURLParam & '&'; + } + strURLParam = strURLParam & lcase( key ) & '=' & trim( arguments.argScope[ key ] ); + intCount++; + } + } + return strURLParam; + } + + /** + * Returns the properties as a struct + */ + public struct function getMemento(){ + var result = {}; + for( var thisProp in getMetaData( this ).properties ){ + if( structKeyExists( variables, thisProp[ 'name' ] ) ){ + result[ thisProp[ 'name' ] ] = variables[ thisProp[ 'name' ] ]; + } + } + return result; + } + + /** + * Generates a struct containing the code verifier and code challenge + * values for use with the PKCE extension. + */ + public struct function generatePKCE(){ + var objPKCE = new utils.deps.pkce.pkce(); + return objPKCE.generatePKCE(); + } + +} \ No newline at end of file