research: SSO-пример из payg-report (Application.cfc, oauth2.cfc, jwt.cfc)
This commit is contained in:
@@ -0,0 +1,522 @@
|
|||||||
|
<cfcomponent
|
||||||
|
displayname="Application"
|
||||||
|
output="true"
|
||||||
|
hint="Handle the application.">
|
||||||
|
|
||||||
|
|
||||||
|
<!--- Pseudoconstructor --->
|
||||||
|
<!--- Set up the application. --->
|
||||||
|
<cfset this.Name = "PAYG-REPORT" />
|
||||||
|
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
|
||||||
|
<cfset this.sessionmanagement="Yes"/>
|
||||||
|
<cfset this.clientmanagement="No"/>
|
||||||
|
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
|
||||||
|
<cfset this.setclientcookies="No"/>
|
||||||
|
<cfset this.mappings = structNew() />
|
||||||
|
<cfset this.mappings["/mod"] = getDirectoryFromPath(getCurrentTemplatePath()) & "mod/" />
|
||||||
|
<cfset this.customTagPaths = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "mod")/>
|
||||||
|
<!--- <cfset this.nullSupport = true/> --->
|
||||||
|
|
||||||
|
<!--- SSO --->
|
||||||
|
<cfset this.client_id = "payg-report.deck.nubes.ru"/>
|
||||||
|
<cfset this.auth_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth" />
|
||||||
|
<cfset this.logout_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/logout" />
|
||||||
|
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
|
||||||
|
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
|
||||||
|
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
|
||||||
|
<cfset this.iam_url = "https://auth-api.ngcloud.ru/api/v1/auth/user"/>
|
||||||
|
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME##CGI.SCRIPT_NAME#"/><!--- куда возвращаться от IDP --->
|
||||||
|
|
||||||
|
<cfset this.datasource = "rpt"/><!--- к буферной базе --->
|
||||||
|
<cfset this.defaultdatasource = this.datasource/>
|
||||||
|
<!--- request scope is available in pseudoconstructor
|
||||||
|
see also https://www.bennadel.com/blog/1437-coldfusion-scope-existence-during-various-request-types-and-events.htm --->
|
||||||
|
<cfset request.DS = "#this.datasource#"/>
|
||||||
|
<cfset request.language=""/>
|
||||||
|
<cfset getDS(this.datasource)/>
|
||||||
|
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
|
||||||
|
<!--- *** проверить, что есть - если есть, не создавать --->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<cffunction
|
||||||
|
name="OnApplicationStart"
|
||||||
|
access="public"
|
||||||
|
returntype="boolean"
|
||||||
|
output="false"
|
||||||
|
hint="Fires when the application is first created.">
|
||||||
|
|
||||||
|
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
|
||||||
|
<!--- Obtain idp certificate OnApplicationStart --->
|
||||||
|
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
|
||||||
|
<cfreturn true />
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
|
||||||
|
<cffunction
|
||||||
|
name="OnRequest"
|
||||||
|
access="public"
|
||||||
|
returntype="void"
|
||||||
|
output="true"
|
||||||
|
hint="Fires after pre page processing is complete.">
|
||||||
|
|
||||||
|
<cfargument name="template" type="string" required="true"/>
|
||||||
|
|
||||||
|
<cfset request.startTickCount=getTickCount()/>
|
||||||
|
|
||||||
|
<cfset setEncoding("FORM", "UTF-8")>
|
||||||
|
<cfset setEncoding("URL", "UTF-8")>
|
||||||
|
|
||||||
|
<cfset request.PERMISSION_NONE=0/>
|
||||||
|
<cfset request.PERMISSION_READ=1/>
|
||||||
|
<cfset request.PERMISSION_WRITE=2/>
|
||||||
|
|
||||||
|
<!--- <cfset request.UNDEFINED_USR_ID=-1/>
|
||||||
|
<cfset request.ANONYMOUS_USR_ID=2/>
|
||||||
|
<cfset request.GUEST_USR_ID=3/> --->
|
||||||
|
|
||||||
|
<!--- global settings --->
|
||||||
|
<cfset request.RECORDS_PER_PAGE=500/>
|
||||||
|
<cfset request.APP_VERSION="0.00.020"/>
|
||||||
|
<cfheader name="X-Application-Version" value=#request.APP_VERSION#/>
|
||||||
|
<cfset request.STAND=getStand()/>
|
||||||
|
|
||||||
|
<!--- application constants --->
|
||||||
|
|
||||||
|
<!---<cfset request.SMTP_SERVER="172.16.16.16"/>
|
||||||
|
<cfset request.MAIL_FROM_FOR_NOTIFICATION="Pipeline tracker <pipeline@mail.ru>"/>
|
||||||
|
<cfset request.SUBJECT_PREFIX="[PIPELINE TRACKER] "/>--->
|
||||||
|
<!---<cfset request.BASE_URL="https://smtest.dtln.local/pipeline/"/>*** сделать нормально. Внимание! При наличии прокси CGI.SERVER_NAME может быть заменено --->
|
||||||
|
|
||||||
|
<cfset local = {} />
|
||||||
|
|
||||||
|
<cfset local.basePath = getDirectoryFromPath(
|
||||||
|
getCurrentTemplatePath()
|
||||||
|
) />
|
||||||
|
|
||||||
|
<cfset local.targetPath = getDirectoryFromPath(
|
||||||
|
expandPath( arguments.template )
|
||||||
|
) />
|
||||||
|
|
||||||
|
<cfset local.requestDepth = (
|
||||||
|
listLen( local.targetPath, "\/" ) -
|
||||||
|
listLen( local.basePath, "\/" )
|
||||||
|
) />
|
||||||
|
|
||||||
|
<cfset request.webRoot = repeatString(
|
||||||
|
"../",
|
||||||
|
local.requestDepth
|
||||||
|
) />
|
||||||
|
|
||||||
|
<!---
|
||||||
|
While we wouldn't normally do this for every page
|
||||||
|
request (it would normally be cached in the
|
||||||
|
application initialization), I'm going to calculate
|
||||||
|
the site URL based on the web root.
|
||||||
|
--->
|
||||||
|
<cfset request.siteUrl = (
|
||||||
|
IIF(
|
||||||
|
(CGI.server_port_secure <!--- CGI.https EQ "On" does not work with apache+tomcat and nginx+tomcat --->),
|
||||||
|
DE( "https://" ),
|
||||||
|
DE( "http://" )
|
||||||
|
) &
|
||||||
|
cgi.http_host &
|
||||||
|
reReplace(
|
||||||
|
getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$",
|
||||||
|
"",
|
||||||
|
"one"
|
||||||
|
)
|
||||||
|
) />
|
||||||
|
|
||||||
|
<cfset request.thisPage=Replace(ReplaceNoCase(expandPath(ARGUMENTS.template), local.basePath, ""), "\", "/")/>
|
||||||
|
<!--- <cfset request.thisUrl="#request.thisPage##len(CGI.queryString)#"/> чревато циклом --->
|
||||||
|
|
||||||
|
<!--- получение сертификата может занимать длительное время.
|
||||||
|
Блокировать на это время приложение или сессию не следует.
|
||||||
|
Вообще не следует ничего блокировать на время сетевого взаимодействия
|
||||||
|
Нижеследующая, на первый взгляд, странная конструкция ровно для этого придумана
|
||||||
|
--->
|
||||||
|
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
||||||
|
<cfset var idpCertificateExists = false/>
|
||||||
|
<cflock scope="application" type="readonly" timeout=1>
|
||||||
|
<cfset request.APP_NAME=this.Name/>
|
||||||
|
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
|
||||||
|
</cflock>
|
||||||
|
|
||||||
|
<cfif NOT idpCertificateExists>
|
||||||
|
<!--- Obtain IDP Certificate (onRequest) --->
|
||||||
|
<cfset var idpCertificate = getIdpCertificate(this.idpCertUrl) />
|
||||||
|
<cflock scope="application" type="exclusive" timeout=1>
|
||||||
|
<cfset application.idpCertificate = idpCertificate/>
|
||||||
|
</cflock>
|
||||||
|
</cfif>
|
||||||
|
<!--- <cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<cfif structKeyExists(url,"logout")>
|
||||||
|
<cflock scope="session" type="exclusive" timeout="3">
|
||||||
|
<cfset structDelete(session, "auth")/>
|
||||||
|
</cflock>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfcookie name="CFID" value="#session.CFID#"/>
|
||||||
|
<cfcookie name="CFTOKEN" value="#session.CFTOKEN#"/>
|
||||||
|
|
||||||
|
<cfinclude template="inc/functions.cfm"/>
|
||||||
|
|
||||||
|
<!--- SSO --->
|
||||||
|
<!--- Если без сессии. Проверяем наличие токена в заголовке (и в куках?)
|
||||||
|
Если его нет, идем к IDP за токеном. Получив токен, складываем его в куки
|
||||||
|
Если токен есть, распаковываем его. Проверяем протухание. Если прошла установленная часть времени жизни, идем за рефрешем (внимание: это сработает только в браузере,
|
||||||
|
если клиент не умеет редиректы - то токен должен быть достаточно свежий)
|
||||||
|
(для начала делаем без рефреша, только проверяем протухание)
|
||||||
|
А рефреш можно сделать на серверной стороне?
|
||||||
|
Кладем данные из токена в реквест и наслаждаемся --->
|
||||||
|
<!--- Вариант: если есть Authorization:bearer, идем с ним к IAM --->
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
|
||||||
|
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('<br>****************** #e.message# : #e.detail# **************');}
|
||||||
|
|
||||||
|
} else { //отправляем браузер к IDP
|
||||||
|
var strURL =idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":request.auth_state,"allow_signup":false});
|
||||||
|
echo('<a href="#strURL#">self-made Auth link (idp) #strURL#</a> <br> <br>');
|
||||||
|
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'; //*******************
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
|
||||||
|
<cfif structKeyExists(request, "auth")>
|
||||||
|
<cfinclude template="#ARGUMENTS.template#"/>
|
||||||
|
<cfelse>
|
||||||
|
<cfset currentDir = (
|
||||||
|
lCase(
|
||||||
|
replace(
|
||||||
|
getDirectoryFromPath(
|
||||||
|
replaceNoCase(
|
||||||
|
expandPath(
|
||||||
|
ARGUMENTS.template
|
||||||
|
),
|
||||||
|
getDirectoryFromPath(
|
||||||
|
getCurrentTemplatePath()
|
||||||
|
),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
), "\", "/", "ALL"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)/>
|
||||||
|
<!--- <cfif currentDir EQ "saml/"><!--- note traling slash --->
|
||||||
|
<cfinclude template="#ARGUMENTS.template#"/>
|
||||||
|
<cfelse>
|
||||||
|
<cfinclude template="login.cfm" />
|
||||||
|
</cfif> --->
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<cfreturn />
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
|
||||||
|
<cffunction
|
||||||
|
name="OnRequestEnd"
|
||||||
|
access="public"
|
||||||
|
returntype="void"
|
||||||
|
output="true"
|
||||||
|
hint="Fires after the page processing is complete.">
|
||||||
|
<!--- Attention! Before CF9, OnrequestEnd is not executed in case of redirect. That is why we use session.save_login --->
|
||||||
|
|
||||||
|
<cfif structKeyExists(session, "save_login")>
|
||||||
|
<cfif session.save_login EQ "">
|
||||||
|
<!--- unset (expire) cookie --->
|
||||||
|
<cfcookie expires="-1" name="portalUser" value="">
|
||||||
|
<cfelse>
|
||||||
|
<!--- set persistent cookie with no expiration --->
|
||||||
|
<cfcookie expires="NEVER" name="portalUser" value="#encrypt(session.save_login,COOKIEENCKEY)#">
|
||||||
|
</cfif>
|
||||||
|
<cfset structDelete(session, "save_login")>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<cfreturn />
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cffunction
|
||||||
|
name="getStand"
|
||||||
|
access="private"
|
||||||
|
returntype="string"
|
||||||
|
output="true"><!--- *** duplicated in svc-api --->
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfquery name=qConfig>
|
||||||
|
select value as stand from config
|
||||||
|
where name='STAND'
|
||||||
|
</cfquery>
|
||||||
|
<cfreturn qConfig.stand/>
|
||||||
|
|
||||||
|
<cfcatch type="ANY">
|
||||||
|
<!--- do nothing, default will be returned --->
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
<cfreturn ""/>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
|
||||||
|
<cffunction
|
||||||
|
name="getDS"
|
||||||
|
access="private"
|
||||||
|
returntype="void"
|
||||||
|
output="true"
|
||||||
|
hint="Configure data source from environment variables (if datasource with the name provided already exists, does nothing). Convention: data source name is an environment varialble prefix">
|
||||||
|
|
||||||
|
<cfargument name="dsname" type="string" required="true"/>
|
||||||
|
<cfargument name="prefix" type="string" default=#dsname#/>
|
||||||
|
|
||||||
|
<cftry>
|
||||||
|
<cfquery name="qTestDs" datasource=#arguments.dsname#>
|
||||||
|
select 1;
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<cfset var ds={}/>
|
||||||
|
<cfset var system = createObject("java", "java.lang.System")/>
|
||||||
|
|
||||||
|
<cfloop list="class,connectionString,database,driver,dbdriver,host,port,type,url,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field"><!--- driver vs dbdriver --->
|
||||||
|
<cfset var value=system.getEnv("#arguments.prefix#_#field#")/>
|
||||||
|
<cfif isDefined("value") AND len(value)>
|
||||||
|
<cfset structInsert(ds,field,value)/>
|
||||||
|
</cfif>
|
||||||
|
</cfloop>
|
||||||
|
<cfif structIsEmpty(ds)>
|
||||||
|
<cfthrow type="application" message="Datasource not configured" detail="Datasource not defined in the environment. Expected prefix is #arguments.prefix#"/>
|
||||||
|
</cfif>
|
||||||
|
|
||||||
|
<!--- test datasource (just to get exception if invalid) --->
|
||||||
|
<cftry>
|
||||||
|
<cfquery name="qTestDsNextTry" datasource=#ds#>
|
||||||
|
select 2;
|
||||||
|
</cfquery>
|
||||||
|
|
||||||
|
<cfcatch type="any">
|
||||||
|
<!--- <cfdump var=#ds#/><cfabort/> --->
|
||||||
|
<cfdump var=#arguments#/>
|
||||||
|
<cfdump var=#ds#/>
|
||||||
|
<cfrethrow/>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
|
||||||
|
<cfset this.datasources["#arguments.dsname#"]=#ds#/> <!--- Интересно, доступен ли здесь this --->
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
<!--- <cfdump var=#ds#/> --->
|
||||||
|
<cfreturn/>
|
||||||
|
</cffunction>
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
//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);
|
||||||
|
}
|
||||||
|
|
||||||
|
</cfscript>
|
||||||
|
|
||||||
|
<cffunction name="rethrow" returntype="void">
|
||||||
|
<!--- https://www.raymondcamden.com/2004/03/09/3089633C-9FA0-606B-3F540AE9642A795F --->
|
||||||
|
<cftry>
|
||||||
|
<cfcatch>
|
||||||
|
<cfrethrow/>
|
||||||
|
</cfcatch>
|
||||||
|
</cftry>
|
||||||
|
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
|
||||||
|
</cffunction>
|
||||||
|
</cfcomponent>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user