research: SSO-пример из payg-report (Application.cfc, oauth2.cfc, jwt.cfc)

This commit is contained in:
2026-05-30 14:19:51 +03:00
parent 6de5631451
commit 374a1a87b6
3 changed files with 977 additions and 0 deletions
+522
View File
@@ -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>
+262
View File
@@ -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;
}
}
+193
View File
@@ -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();
}
}