You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1612 line
57KB

  1. <%
  2. '==============================================================================
  3. ' Keycloak OpenID Connect helper
  4. '==============================================================================
  5. ' Typical flow:
  6. ' 1. Configure KeycloakBaseUrl/Realm/ClientId/ClientSecret/RedirectUri in
  7. ' public/web.config, or set those properties on KeycloakAuth().
  8. ' 2. Send users to KeycloakLogin().
  9. ' 3. Point the Keycloak redirect URI at an action that calls
  10. ' KeycloakHandleCallback().
  11. ' 4. Use KeycloakCurrentUser(), KeycloakAccessToken(), or KeycloakUserInfo().
  12. '==============================================================================
  13. Class KeycloakAuth_Class
  14. Public BaseUrl
  15. Public Realm
  16. Public ClientId
  17. Public ClientSecret
  18. Public RedirectUri
  19. Public Scope
  20. Public LogoutRedirectUri
  21. Public SessionPrefix
  22. Public PendingLoginCookieMinutes
  23. Public AllowedClockSkewSeconds
  24. Public HttpResolveTimeoutMs
  25. Public HttpConnectTimeoutMs
  26. Public HttpSendTimeoutMs
  27. Public HttpReceiveTimeoutMs
  28. Public EnableDiagnosticLogging
  29. Public DiagnosticLogPath
  30. Public ErrorMessage
  31. Public LastStatus
  32. Public LastResponseText
  33. Public TokenResponse
  34. Public UserInfo
  35. Private Sub Class_Initialize()
  36. BaseUrl = ""
  37. Realm = ""
  38. ClientId = ""
  39. ClientSecret = ""
  40. RedirectUri = ""
  41. Scope = "openid profile email"
  42. LogoutRedirectUri = ""
  43. SessionPrefix = "Keycloak_"
  44. PendingLoginCookieMinutes = 15
  45. AllowedClockSkewSeconds = 300
  46. HttpResolveTimeoutMs = 5000
  47. HttpConnectTimeoutMs = 5000
  48. HttpSendTimeoutMs = 15000
  49. HttpReceiveTimeoutMs = 15000
  50. EnableDiagnosticLogging = False
  51. DiagnosticLogPath = ""
  52. ErrorMessage = ""
  53. LastStatus = 0
  54. LastResponseText = ""
  55. Set TokenResponse = Nothing
  56. Set UserInfo = Nothing
  57. End Sub
  58. Public Sub Configure(ByVal keycloakBaseUrl, ByVal keycloakRealm, ByVal keycloakClientId, ByVal keycloakClientSecret, ByVal keycloakRedirectUri)
  59. BaseUrl = Trim(CStr(keycloakBaseUrl))
  60. Realm = Trim(CStr(keycloakRealm))
  61. ClientId = Trim(CStr(keycloakClientId))
  62. ClientSecret = CStr(keycloakClientSecret)
  63. RedirectUri = Trim(CStr(keycloakRedirectUri))
  64. End Sub
  65. Public Sub ConfigureFromAppSettings()
  66. Dim sharedHttpTimeoutMs
  67. BaseUrl = KeycloakReadAppSetting("KeycloakBaseUrl", BaseUrl)
  68. Realm = KeycloakReadAppSetting("KeycloakRealm", Realm)
  69. ClientId = KeycloakReadAppSetting("KeycloakClientId", ClientId)
  70. ClientSecret = KeycloakReadAppSetting("KeycloakClientSecret", ClientSecret)
  71. RedirectUri = KeycloakReadAppSetting("KeycloakRedirectUri", RedirectUri)
  72. Scope = KeycloakReadAppSetting("KeycloakScope", Scope)
  73. LogoutRedirectUri = KeycloakReadAppSetting("KeycloakLogoutRedirectUri", LogoutRedirectUri)
  74. PendingLoginCookieMinutes = KeycloakReadAppSettingLong("KeycloakPendingLoginCookieMinutes", PendingLoginCookieMinutes)
  75. AllowedClockSkewSeconds = KeycloakReadAppSettingLong("KeycloakAllowedClockSkewSeconds", AllowedClockSkewSeconds)
  76. sharedHttpTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpTimeoutMs", 0)
  77. If sharedHttpTimeoutMs > 0 Then
  78. HttpResolveTimeoutMs = sharedHttpTimeoutMs
  79. HttpConnectTimeoutMs = sharedHttpTimeoutMs
  80. HttpSendTimeoutMs = sharedHttpTimeoutMs
  81. HttpReceiveTimeoutMs = sharedHttpTimeoutMs
  82. End If
  83. HttpResolveTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpResolveTimeoutMs", HttpResolveTimeoutMs)
  84. HttpConnectTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpConnectTimeoutMs", HttpConnectTimeoutMs)
  85. HttpSendTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpSendTimeoutMs", HttpSendTimeoutMs)
  86. HttpReceiveTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpReceiveTimeoutMs", HttpReceiveTimeoutMs)
  87. EnableDiagnosticLogging = KeycloakReadAppSettingBool("KeycloakEnableLogging", KeycloakReadAppSettingBool("EnableErrorLogging", EnableDiagnosticLogging))
  88. DiagnosticLogPath = KeycloakReadAppSetting("KeycloakLogPath", KeycloakReadAppSetting("ErrorLogPath", DiagnosticLogPath))
  89. End Sub
  90. Public Function IsConfigured()
  91. IsConfigured = (Len(NormalizeBaseUrl(BaseUrl)) > 0 And Len(Realm) > 0 And Len(ClientId) > 0 And Len(RedirectUri) > 0)
  92. End Function
  93. Public Function ValidateOperationalConfiguration(ByVal environmentName)
  94. Dim normalizedEnvironment
  95. ValidateOperationalConfiguration = True
  96. ErrorMessage = ""
  97. normalizedEnvironment = LCase(Trim(CStr(environmentName)))
  98. If normalizedEnvironment <> "production" Then Exit Function
  99. If Not IsHttpsUrl(BaseUrl) Then
  100. ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakBaseUrl."
  101. ValidateOperationalConfiguration = False
  102. Exit Function
  103. End If
  104. If UrlTargetsLocalhost(BaseUrl) Then
  105. ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakBaseUrl."
  106. ValidateOperationalConfiguration = False
  107. Exit Function
  108. End If
  109. If Not IsHttpsUrl(RedirectUri) Then
  110. ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakRedirectUri."
  111. ValidateOperationalConfiguration = False
  112. Exit Function
  113. End If
  114. If UrlTargetsLocalhost(RedirectUri) Then
  115. ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakRedirectUri."
  116. ValidateOperationalConfiguration = False
  117. Exit Function
  118. End If
  119. If Len(Trim(CStr(LogoutRedirectUri))) > 0 Then
  120. If Not IsHttpsUrl(LogoutRedirectUri) Then
  121. ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakLogoutRedirectUri."
  122. ValidateOperationalConfiguration = False
  123. Exit Function
  124. End If
  125. If UrlTargetsLocalhost(LogoutRedirectUri) Then
  126. ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakLogoutRedirectUri."
  127. ValidateOperationalConfiguration = False
  128. Exit Function
  129. End If
  130. End If
  131. End Function
  132. Public Function RealmBaseUrl()
  133. If Len(NormalizeBaseUrl(BaseUrl)) = 0 Or Len(Realm) = 0 Then
  134. RealmBaseUrl = ""
  135. Exit Function
  136. End If
  137. RealmBaseUrl = NormalizeBaseUrl(BaseUrl) & "/realms/" & Realm
  138. End Function
  139. Public Function AuthorizationEndpoint()
  140. AuthorizationEndpoint = RealmBaseUrl() & "/protocol/openid-connect/auth"
  141. End Function
  142. Public Function TokenEndpoint()
  143. TokenEndpoint = RealmBaseUrl() & "/protocol/openid-connect/token"
  144. End Function
  145. Public Function UserInfoEndpoint()
  146. UserInfoEndpoint = RealmBaseUrl() & "/protocol/openid-connect/userinfo"
  147. End Function
  148. Public Function LogoutEndpoint()
  149. LogoutEndpoint = RealmBaseUrl() & "/protocol/openid-connect/logout"
  150. End Function
  151. Public Function BuildLoginUrl(ByVal state, ByVal nonce)
  152. Dim loginUrl
  153. loginUrl = AuthorizationEndpoint()
  154. loginUrl = loginUrl & "?client_id=" & UrlEncode(ClientId)
  155. loginUrl = loginUrl & "&response_type=code"
  156. loginUrl = loginUrl & "&scope=" & UrlEncode(Scope)
  157. loginUrl = loginUrl & "&redirect_uri=" & UrlEncode(RedirectUri)
  158. If Len(state) > 0 Then loginUrl = loginUrl & "&state=" & UrlEncode(state)
  159. If Len(nonce) > 0 Then loginUrl = loginUrl & "&nonce=" & UrlEncode(nonce)
  160. BuildLoginUrl = loginUrl
  161. End Function
  162. Public Sub Login()
  163. Dim state, nonce, loginUrl
  164. If Not IsConfigured() Then
  165. Err.Raise vbObjectError + 5100, "KeycloakAuth.Login", "Keycloak is not configured. Set KeycloakBaseUrl, KeycloakRealm, KeycloakClientId, and KeycloakRedirectUri."
  166. End If
  167. If Not ValidateOperationalConfiguration(KeycloakReadAppSetting("Environment", "Development")) Then
  168. Call LogDiagnostic("ERROR", "config.invalid_for_environment", ErrorMessage, "environment=" & SafeLogValue(KeycloakReadAppSetting("Environment", "Development")))
  169. Err.Raise vbObjectError + 5101, "KeycloakAuth.Login", ErrorMessage
  170. End If
  171. Call ClearPendingLoginArtifacts()
  172. state = CreateRandomValue()
  173. nonce = CreateRandomValue()
  174. loginUrl = BuildLoginUrl(state, nonce)
  175. Call WritePendingLoginCookie(SessionPrefix & "State", state)
  176. Call WritePendingLoginCookie(SessionPrefix & "Nonce", nonce)
  177. Call LogDiagnostic("INFO", "login.pending_values_issued", "Issued new Keycloak login state and nonce.", "state=" & SensitiveValueSummary(state) & " nonce=" & SensitiveValueSummary(nonce))
  178. Response.Redirect loginUrl
  179. End Sub
  180. Public Function HandleCallback()
  181. Dim callbackError, callbackDescription, code, state, expectedNonce, accessToken, userData
  182. HandleCallback = False
  183. ErrorMessage = ""
  184. callbackError = Trim(CStr(Request.QueryString("error")))
  185. If Len(callbackError) > 0 Then
  186. callbackDescription = Trim(CStr(Request.QueryString("error_description")))
  187. ErrorMessage = "Keycloak returned an error: " & callbackError
  188. If Len(callbackDescription) > 0 Then ErrorMessage = ErrorMessage & " - " & callbackDescription
  189. Exit Function
  190. End If
  191. code = Trim(CStr(Request.QueryString("code")))
  192. If Len(code) = 0 Then
  193. ErrorMessage = "Keycloak callback did not include an authorization code."
  194. Call ClearPendingLoginArtifacts()
  195. Exit Function
  196. End If
  197. state = Trim(CStr(Request.QueryString("state")))
  198. If Not ValidateState(state) Then
  199. ErrorMessage = StateValidationError(state)
  200. Call ClearPendingLoginArtifacts()
  201. Exit Function
  202. End If
  203. expectedNonce = ReadRequestCookie(SessionPrefix & "Nonce")
  204. If Len(expectedNonce) = 0 Then
  205. ErrorMessage = "The login session nonce is missing, so the Keycloak callback cannot be validated."
  206. Call ClearPendingLoginArtifacts()
  207. Exit Function
  208. End If
  209. If Not ExchangeCode(code, expectedNonce) Then
  210. Call ClearPendingLoginArtifacts()
  211. Exit Function
  212. End If
  213. accessToken = DictionaryString(TokenResponse, "access_token")
  214. If Len(accessToken) > 0 Then
  215. Set userData = GetUserInfo(accessToken)
  216. If userData Is Nothing And Len(ErrorMessage) > 0 Then
  217. Call LogDiagnostic("WARN", "userinfo.unavailable", ErrorMessage, "status=" & CStr(LastStatus))
  218. ErrorMessage = ""
  219. End If
  220. End If
  221. Call ClearPendingLoginArtifacts()
  222. HandleCallback = True
  223. End Function
  224. Public Function ExchangeCode(ByVal code, ByVal expectedNonce)
  225. Dim body, tokenData
  226. ExchangeCode = False
  227. ErrorMessage = ""
  228. If Not IsConfigured() Then
  229. ErrorMessage = "Keycloak is not configured."
  230. Exit Function
  231. End If
  232. body = ""
  233. body = AddFormValue(body, "grant_type", "authorization_code")
  234. body = AddFormValue(body, "client_id", ClientId)
  235. body = AddFormValue(body, "client_secret", ClientSecret)
  236. body = AddFormValue(body, "code", code)
  237. body = AddFormValue(body, "redirect_uri", RedirectUri)
  238. Set tokenData = RequestToken(body)
  239. If tokenData Is Nothing Then Exit Function
  240. If Not ValidateTokenResponse(tokenData, True, expectedNonce) Then Exit Function
  241. Set TokenResponse = tokenData
  242. Call StoreTokenResponse(tokenData)
  243. ExchangeCode = True
  244. End Function
  245. Public Function RefreshAccessToken(ByVal refreshToken)
  246. Dim body, tokenData
  247. RefreshAccessToken = False
  248. ErrorMessage = ""
  249. If Len(refreshToken) = 0 Then refreshToken = GetSessionRefreshToken()
  250. If Len(refreshToken) = 0 Then
  251. ErrorMessage = "No refresh token is available."
  252. Exit Function
  253. End If
  254. body = ""
  255. body = AddFormValue(body, "grant_type", "refresh_token")
  256. body = AddFormValue(body, "client_id", ClientId)
  257. body = AddFormValue(body, "client_secret", ClientSecret)
  258. body = AddFormValue(body, "refresh_token", refreshToken)
  259. Set tokenData = RequestToken(body)
  260. If tokenData Is Nothing Then Exit Function
  261. If Not ValidateTokenResponse(tokenData, False, "") Then Exit Function
  262. Set TokenResponse = tokenData
  263. Call StoreTokenResponse(tokenData)
  264. RefreshAccessToken = True
  265. End Function
  266. Public Function GetUserInfo(ByVal accessToken)
  267. Dim parsed
  268. Set GetUserInfo = Nothing
  269. ErrorMessage = ""
  270. If Len(accessToken) = 0 Then accessToken = GetSessionAccessToken()
  271. If Len(accessToken) = 0 Then
  272. ErrorMessage = "No access token is available."
  273. Exit Function
  274. End If
  275. If Not SendBearerGet(UserInfoEndpoint(), accessToken) Then Exit Function
  276. If Not IsSuccessStatus(LastStatus) Then
  277. ErrorMessage = "Keycloak userinfo request failed with HTTP " & LastStatus & "."
  278. Exit Function
  279. End If
  280. Set parsed = ParseJsonObject(LastResponseText)
  281. If parsed Is Nothing Then Exit Function
  282. Set UserInfo = parsed
  283. Session(SessionPrefix & "UserInfoJson") = LastResponseText
  284. Set GetUserInfo = parsed
  285. End Function
  286. Public Function CurrentUser()
  287. Dim userInfoJson, idToken
  288. Set CurrentUser = Nothing
  289. If IsObject(UserInfo) Then
  290. If Not UserInfo Is Nothing Then
  291. Set CurrentUser = UserInfo
  292. Exit Function
  293. End If
  294. End If
  295. userInfoJson = ReadSessionValue(SessionPrefix & "UserInfoJson")
  296. If Len(userInfoJson) > 0 Then
  297. Set UserInfo = ParseJsonObject(userInfoJson)
  298. If Not UserInfo Is Nothing Then
  299. Set CurrentUser = UserInfo
  300. Exit Function
  301. End If
  302. End If
  303. idToken = GetSessionIdToken()
  304. If Len(idToken) > 0 Then
  305. Set CurrentUser = GetTokenClaims(idToken)
  306. End If
  307. End Function
  308. Public Function GetTokenClaims(ByVal token)
  309. Dim parts, payloadJson
  310. Set GetTokenClaims = Nothing
  311. If Len(token) = 0 Then Exit Function
  312. parts = Split(token, ".")
  313. If UBound(parts) < 1 Then
  314. ErrorMessage = "The token is not a JWT."
  315. Exit Function
  316. End If
  317. payloadJson = Base64UrlDecodeToString(parts(1))
  318. If Len(payloadJson) = 0 Then Exit Function
  319. Set GetTokenClaims = ParseJsonObject(payloadJson)
  320. End Function
  321. Public Function RoleClaims()
  322. Dim idToken
  323. Set RoleClaims = Nothing
  324. idToken = GetSessionIdToken()
  325. If Len(idToken) > 0 Then
  326. Set RoleClaims = GetTokenClaims(idToken)
  327. If Not RoleClaims Is Nothing Then Exit Function
  328. End If
  329. Set RoleClaims = CurrentUser()
  330. End Function
  331. Public Function HasRealmRole(ByVal roleName)
  332. Dim claims, realmAccess
  333. HasRealmRole = False
  334. Set claims = RoleClaims()
  335. If claims Is Nothing Then Exit Function
  336. Set realmAccess = NestedDictionary(claims, "realm_access")
  337. If realmAccess Is Nothing Then Exit Function
  338. HasRealmRole = CollectionContainsText(NestedDictionary(realmAccess, "roles"), roleName)
  339. End Function
  340. Public Function HasClientRole(ByVal clientId, ByVal roleName)
  341. Dim claims, resourceAccess, clientAccess
  342. HasClientRole = False
  343. Set claims = RoleClaims()
  344. If claims Is Nothing Then Exit Function
  345. Set resourceAccess = NestedDictionary(claims, "resource_access")
  346. If resourceAccess Is Nothing Then Exit Function
  347. Set clientAccess = NestedDictionary(resourceAccess, clientId)
  348. If clientAccess Is Nothing Then Exit Function
  349. HasClientRole = CollectionContainsText(NestedDictionary(clientAccess, "roles"), roleName)
  350. End Function
  351. Public Function RequireLogin(ByVal returnToPath)
  352. RequireLogin = True
  353. If IsLoggedIn() Then Exit Function
  354. If Len(Trim(CStr(returnToPath))) = 0 Then
  355. returnToPath = CurrentRequestPathAndQuery()
  356. End If
  357. Call SetPostLoginRedirectPath(returnToPath)
  358. Call Login()
  359. RequireLogin = False
  360. End Function
  361. Public Sub SetPostLoginRedirectPath(ByVal returnToPath)
  362. Dim normalizedPath
  363. normalizedPath = NormalizeRelativeReturnPath(returnToPath)
  364. If Len(normalizedPath) = 0 Then
  365. Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
  366. Exit Sub
  367. End If
  368. If IsAuthPath(normalizedPath) Then
  369. Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
  370. Exit Sub
  371. End If
  372. Session(SessionPrefix & "PostLoginRedirectPath") = normalizedPath
  373. End Sub
  374. Public Function ConsumePostLoginRedirectPath(ByVal fallbackPath)
  375. Dim storedPath, normalizedFallback
  376. storedPath = NormalizeRelativeReturnPath(ReadSessionValue(SessionPrefix & "PostLoginRedirectPath"))
  377. Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
  378. If Len(storedPath) > 0 Then
  379. ConsumePostLoginRedirectPath = storedPath
  380. Exit Function
  381. End If
  382. normalizedFallback = NormalizeRelativeReturnPath(fallbackPath)
  383. If Len(normalizedFallback) > 0 Then
  384. ConsumePostLoginRedirectPath = normalizedFallback
  385. Else
  386. ConsumePostLoginRedirectPath = "/"
  387. End If
  388. End Function
  389. Public Function NormalizeRelativeReturnPath(ByVal returnToPath)
  390. Dim normalizedPath
  391. NormalizeRelativeReturnPath = ""
  392. normalizedPath = Trim(CStr(returnToPath))
  393. If Len(normalizedPath) = 0 Then Exit Function
  394. normalizedPath = Replace(normalizedPath, vbCr, "")
  395. normalizedPath = Replace(normalizedPath, vbLf, "")
  396. If InStr(1, normalizedPath, "://", vbBinaryCompare) > 0 Then Exit Function
  397. If Left(normalizedPath, 2) = "//" Then Exit Function
  398. If Left(normalizedPath, 1) = "?" Then
  399. normalizedPath = "/" & normalizedPath
  400. End If
  401. If Left(normalizedPath, 1) <> "/" Then Exit Function
  402. NormalizeRelativeReturnPath = normalizedPath
  403. End Function
  404. Public Function ValidateIdToken(ByVal token, ByVal expectedNonce, ByVal requireNonce)
  405. Dim claims
  406. ValidateIdToken = False
  407. ErrorMessage = ""
  408. If Len(token) = 0 Then
  409. ErrorMessage = "Keycloak did not return an ID token."
  410. Call LogDiagnostic("ERROR", "id_token.missing", ErrorMessage, "")
  411. Exit Function
  412. End If
  413. Set claims = GetTokenClaims(token)
  414. If claims Is Nothing Then
  415. Call LogDiagnostic("ERROR", "id_token.decode_failed", ErrorMessage, "")
  416. Exit Function
  417. End If
  418. ValidateIdToken = ValidateIdTokenClaims(claims, expectedNonce, requireNonce)
  419. End Function
  420. Public Function IsLoggedIn()
  421. IsLoggedIn = (Len(GetSessionAccessToken()) > 0)
  422. End Function
  423. Public Function GetSessionAccessToken()
  424. GetSessionAccessToken = ReadSessionValue(SessionPrefix & "AccessToken")
  425. End Function
  426. Public Function GetSessionRefreshToken()
  427. GetSessionRefreshToken = ReadSessionValue(SessionPrefix & "RefreshToken")
  428. End Function
  429. Public Function GetSessionIdToken()
  430. GetSessionIdToken = ReadSessionValue(SessionPrefix & "IdToken")
  431. End Function
  432. Public Function BuildLogoutUrl(ByVal postLogoutRedirectUri)
  433. Dim logoutUrl, idToken
  434. logoutUrl = LogoutEndpoint()
  435. If Len(logoutUrl) = 0 Then
  436. BuildLogoutUrl = ""
  437. Exit Function
  438. End If
  439. If Len(postLogoutRedirectUri) = 0 Then postLogoutRedirectUri = LogoutRedirectUri
  440. logoutUrl = logoutUrl & "?client_id=" & UrlEncode(ClientId)
  441. idToken = GetSessionIdToken()
  442. If Len(idToken) > 0 Then logoutUrl = logoutUrl & "&id_token_hint=" & UrlEncode(idToken)
  443. If Len(postLogoutRedirectUri) > 0 Then logoutUrl = logoutUrl & "&post_logout_redirect_uri=" & UrlEncode(postLogoutRedirectUri)
  444. BuildLogoutUrl = logoutUrl
  445. End Function
  446. Public Sub Logout(ByVal postLogoutRedirectUri)
  447. Dim logoutUrl
  448. logoutUrl = BuildLogoutUrl(postLogoutRedirectUri)
  449. Call ClearSession()
  450. If Len(logoutUrl) > 0 Then Response.Redirect logoutUrl
  451. End Sub
  452. Public Sub ClearSession()
  453. On Error Resume Next
  454. Session.Contents.Remove(SessionPrefix & "AccessToken")
  455. Session.Contents.Remove(SessionPrefix & "RefreshToken")
  456. Session.Contents.Remove(SessionPrefix & "IdToken")
  457. Session.Contents.Remove(SessionPrefix & "TokenType")
  458. Session.Contents.Remove(SessionPrefix & "ExpiresAt")
  459. Session.Contents.Remove(SessionPrefix & "RefreshExpiresAt")
  460. Session.Contents.Remove(SessionPrefix & "UserInfoJson")
  461. Call ClearPendingLoginArtifacts()
  462. Set TokenResponse = Nothing
  463. Set UserInfo = Nothing
  464. On Error GoTo 0
  465. End Sub
  466. Private Function RequestToken(ByVal body)
  467. Dim parsedToken
  468. Set RequestToken = Nothing
  469. If Not SendFormPost(TokenEndpoint(), body) Then Exit Function
  470. If Not IsSuccessStatus(LastStatus) Then
  471. ErrorMessage = "Keycloak token request failed with HTTP " & LastStatus & "."
  472. Call LogDiagnostic("WARN", "token.http_error", ErrorMessage, BuildHttpDetail("POST", TokenEndpoint(), LastStatus, LastResponseText))
  473. Exit Function
  474. End If
  475. Set parsedToken = ParseJsonObject(LastResponseText)
  476. If parsedToken Is Nothing Then
  477. Call LogDiagnostic("ERROR", "token.parse_failed", ErrorMessage, BuildHttpDetail("POST", TokenEndpoint(), LastStatus, LastResponseText))
  478. Exit Function
  479. End If
  480. Set RequestToken = parsedToken
  481. End Function
  482. Private Sub StoreTokenResponse(ByVal tokenData)
  483. Dim expiresIn, refreshExpiresIn
  484. If tokenData Is Nothing Then Exit Sub
  485. If tokenData.Exists("access_token") Then Session(SessionPrefix & "AccessToken") = CStr(tokenData.Item("access_token"))
  486. If tokenData.Exists("refresh_token") Then Session(SessionPrefix & "RefreshToken") = CStr(tokenData.Item("refresh_token"))
  487. If tokenData.Exists("id_token") Then Session(SessionPrefix & "IdToken") = CStr(tokenData.Item("id_token"))
  488. If tokenData.Exists("token_type") Then Session(SessionPrefix & "TokenType") = CStr(tokenData.Item("token_type"))
  489. If tokenData.Exists("expires_in") Then
  490. expiresIn = CLng(tokenData.Item("expires_in"))
  491. Session(SessionPrefix & "ExpiresAt") = DateAdd("s", expiresIn, Now())
  492. End If
  493. If tokenData.Exists("refresh_expires_in") Then
  494. refreshExpiresIn = CLng(tokenData.Item("refresh_expires_in"))
  495. Session(SessionPrefix & "RefreshExpiresAt") = DateAdd("s", refreshExpiresIn, Now())
  496. End If
  497. End Sub
  498. Private Function ValidateState(ByVal callbackState)
  499. ValidateState = StatesMatch(ReadRequestCookie(SessionPrefix & "State"), callbackState)
  500. End Function
  501. Public Function StateValidationError(ByVal callbackState)
  502. Dim expectedState
  503. StateValidationError = ""
  504. expectedState = ReadRequestCookie(SessionPrefix & "State")
  505. If Len(NormalizeStateValue(expectedState)) = 0 Then
  506. StateValidationError = "Keycloak callback could not be matched to an active login session because the stored login state is missing. This usually means the login cookie expired, the callback URL was reloaded, or the callback was opened directly."
  507. Exit Function
  508. End If
  509. If Len(NormalizeStateValue(callbackState)) = 0 Then
  510. StateValidationError = "Keycloak callback did not include a state value."
  511. Exit Function
  512. End If
  513. StateValidationError = "Keycloak callback state did not match the active login session. This usually means another login attempt replaced the stored state or an older callback URL was replayed."
  514. End Function
  515. Private Function ValidateTokenResponse(ByVal tokenData, ByVal requireNonce, ByVal expectedNonce)
  516. Dim accessToken, tokenType, idToken
  517. ValidateTokenResponse = False
  518. If tokenData Is Nothing Then Exit Function
  519. accessToken = DictionaryString(tokenData, "access_token")
  520. If Len(accessToken) = 0 Then
  521. ErrorMessage = "Keycloak token response did not include an access token."
  522. Call LogDiagnostic("ERROR", "token.access_token_missing", ErrorMessage, "")
  523. Exit Function
  524. End If
  525. tokenType = DictionaryString(tokenData, "token_type")
  526. If Len(tokenType) > 0 Then
  527. If StrComp(tokenType, "Bearer", vbTextCompare) <> 0 Then
  528. ErrorMessage = "Keycloak token response returned an unexpected token type."
  529. Call LogDiagnostic("ERROR", "token.invalid_type", ErrorMessage, "token_type=" & SafeLogValue(tokenType))
  530. Exit Function
  531. End If
  532. End If
  533. idToken = DictionaryString(tokenData, "id_token")
  534. If requireNonce Then
  535. If Len(idToken) = 0 Then
  536. ErrorMessage = "Keycloak token response did not include an ID token for nonce validation."
  537. Call LogDiagnostic("ERROR", "id_token.required_for_nonce", ErrorMessage, "")
  538. Exit Function
  539. End If
  540. End If
  541. If Len(idToken) > 0 Then
  542. If Not ValidateIdToken(idToken, expectedNonce, requireNonce) Then Exit Function
  543. End If
  544. ValidateTokenResponse = True
  545. End Function
  546. Private Function ValidateIdTokenClaims(ByVal claims, ByVal expectedNonce, ByVal requireNonce)
  547. Dim issuer, subject, nonceValue, authorizedParty
  548. Dim nowEpoch, expiresAt, notBefore, issuedAt, audienceCount
  549. ValidateIdTokenClaims = False
  550. If claims Is Nothing Then Exit Function
  551. issuer = DictionaryString(claims, "iss")
  552. If Len(issuer) = 0 Then
  553. ErrorMessage = "Keycloak ID token is missing the issuer claim."
  554. Call LogDiagnostic("ERROR", "id_token.iss_missing", ErrorMessage, "")
  555. Exit Function
  556. End If
  557. If StrComp(issuer, RealmBaseUrl(), vbBinaryCompare) <> 0 Then
  558. ErrorMessage = "Keycloak ID token issuer did not match the configured realm."
  559. Call LogDiagnostic("ERROR", "id_token.iss_invalid", ErrorMessage, "expected_iss=" & SafeLogValue(RealmBaseUrl()) & " actual_iss=" & SafeLogValue(issuer))
  560. Exit Function
  561. End If
  562. subject = DictionaryString(claims, "sub")
  563. If Len(subject) = 0 Then
  564. ErrorMessage = "Keycloak ID token is missing the subject claim."
  565. Call LogDiagnostic("ERROR", "id_token.sub_missing", ErrorMessage, "")
  566. Exit Function
  567. End If
  568. If Not AudienceContainsClientId(claims, audienceCount) Then
  569. ErrorMessage = "Keycloak ID token audience did not include the configured client."
  570. Call LogDiagnostic("ERROR", "id_token.aud_invalid", ErrorMessage, "client_id=" & SafeLogValue(ClientId) & " aud=" & SafeLogValue(ClaimValueForLog(claims, "aud")))
  571. Exit Function
  572. End If
  573. If audienceCount > 1 Then
  574. authorizedParty = DictionaryString(claims, "azp")
  575. If Len(authorizedParty) = 0 Or StrComp(authorizedParty, ClientId, vbBinaryCompare) <> 0 Then
  576. ErrorMessage = "Keycloak ID token authorized party did not match the configured client."
  577. Call LogDiagnostic("ERROR", "id_token.azp_invalid", ErrorMessage, "client_id=" & SafeLogValue(ClientId) & " azp=" & SafeLogValue(authorizedParty))
  578. Exit Function
  579. End If
  580. End If
  581. If Not TryGetNumericClaim(claims, "exp", expiresAt) Then
  582. ErrorMessage = "Keycloak ID token is missing the expiration claim."
  583. Call LogDiagnostic("ERROR", "id_token.exp_missing", ErrorMessage, "")
  584. Exit Function
  585. End If
  586. nowEpoch = CurrentUnixTime()
  587. If (expiresAt + CLng(AllowedClockSkewSeconds)) < nowEpoch Then
  588. ErrorMessage = "Keycloak ID token has expired."
  589. Call LogDiagnostic("ERROR", "id_token.expired", ErrorMessage, "exp=" & CStr(expiresAt) & " now=" & CStr(nowEpoch))
  590. Exit Function
  591. End If
  592. If TryGetNumericClaim(claims, "nbf", notBefore) Then
  593. If (notBefore - CLng(AllowedClockSkewSeconds)) > nowEpoch Then
  594. ErrorMessage = "Keycloak ID token is not valid yet."
  595. Call LogDiagnostic("ERROR", "id_token.not_yet_valid", ErrorMessage, "nbf=" & CStr(notBefore) & " now=" & CStr(nowEpoch))
  596. Exit Function
  597. End If
  598. End If
  599. If TryGetNumericClaim(claims, "iat", issuedAt) Then
  600. If (issuedAt - CLng(AllowedClockSkewSeconds)) > nowEpoch Then
  601. ErrorMessage = "Keycloak ID token appears to have been issued in the future."
  602. Call LogDiagnostic("ERROR", "id_token.issued_at_invalid", ErrorMessage, "iat=" & CStr(issuedAt) & " now=" & CStr(nowEpoch))
  603. Exit Function
  604. End If
  605. End If
  606. If requireNonce Then
  607. If Len(expectedNonce) = 0 Then
  608. ErrorMessage = "The login session nonce is missing, so the Keycloak callback cannot be validated."
  609. Call LogDiagnostic("ERROR", "id_token.nonce_session_missing", ErrorMessage, "")
  610. Exit Function
  611. End If
  612. nonceValue = DictionaryString(claims, "nonce")
  613. If Len(nonceValue) = 0 Then
  614. ErrorMessage = "Keycloak ID token is missing the nonce claim."
  615. Call LogDiagnostic("ERROR", "id_token.nonce_missing", ErrorMessage, "")
  616. Exit Function
  617. End If
  618. If StrComp(nonceValue, expectedNonce, vbBinaryCompare) <> 0 Then
  619. ErrorMessage = "Keycloak ID token nonce did not match the active login session."
  620. Call LogDiagnostic("ERROR", "id_token.nonce_invalid", ErrorMessage, "expected_nonce=" & SensitiveValueSummary(expectedNonce) & " actual_nonce=" & SensitiveValueSummary(nonceValue))
  621. Exit Function
  622. End If
  623. End If
  624. ValidateIdTokenClaims = True
  625. End Function
  626. Private Function SendFormPost(ByVal url, ByVal body)
  627. Dim http
  628. SendFormPost = False
  629. LastStatus = 0
  630. LastResponseText = ""
  631. Set http = CreateHttpClient()
  632. If http Is Nothing Then Exit Function
  633. On Error Resume Next
  634. http.open "POST", url, False
  635. http.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
  636. http.setRequestHeader "Accept", "application/json"
  637. http.send body
  638. If Err.Number <> 0 Then
  639. ErrorMessage = "Keycloak HTTP POST failed: " & Err.Description
  640. Call LogDiagnostic("ERROR", "http.post_failed", ErrorMessage, BuildHttpDetail("POST", url, 0, ""))
  641. Err.Clear
  642. Else
  643. LastStatus = CLng(http.status)
  644. LastResponseText = CStr(http.responseText)
  645. SendFormPost = True
  646. End If
  647. Set http = Nothing
  648. On Error GoTo 0
  649. End Function
  650. Private Function SendBearerGet(ByVal url, ByVal bearerToken)
  651. Dim http
  652. SendBearerGet = False
  653. LastStatus = 0
  654. LastResponseText = ""
  655. Set http = CreateHttpClient()
  656. If http Is Nothing Then Exit Function
  657. On Error Resume Next
  658. http.open "GET", url, False
  659. http.setRequestHeader "Accept", "application/json"
  660. http.setRequestHeader "Authorization", "Bearer " & bearerToken
  661. http.send
  662. If Err.Number <> 0 Then
  663. ErrorMessage = "Keycloak HTTP GET failed: " & Err.Description
  664. Call LogDiagnostic("ERROR", "http.get_failed", ErrorMessage, BuildHttpDetail("GET", url, 0, ""))
  665. Err.Clear
  666. Else
  667. LastStatus = CLng(http.status)
  668. LastResponseText = CStr(http.responseText)
  669. SendBearerGet = True
  670. End If
  671. Set http = Nothing
  672. On Error GoTo 0
  673. End Function
  674. Private Function ParseJsonObject(ByVal jsonText)
  675. Dim parser
  676. Set ParseJsonObject = Nothing
  677. If Len(Trim(CStr(jsonText))) = 0 Then Exit Function
  678. On Error Resume Next
  679. Set parser = New aspJSON
  680. parser.loadJSON jsonText
  681. If Err.Number <> 0 Then
  682. ErrorMessage = "Unable to parse Keycloak JSON response: " & Err.Description
  683. Call LogDiagnostic("ERROR", "json.parse_failed", ErrorMessage, "json_length=" & CStr(Len(CStr(jsonText))))
  684. Err.Clear
  685. Else
  686. Set ParseJsonObject = parser.data
  687. End If
  688. On Error GoTo 0
  689. End Function
  690. Private Function Base64UrlDecodeToString(ByVal value)
  691. Dim base64Value, xml, node, bytes, stream
  692. Base64UrlDecodeToString = ""
  693. base64Value = Replace(Replace(CStr(value), "-", "+"), "_", "/")
  694. Do While (Len(base64Value) Mod 4) <> 0
  695. base64Value = base64Value & "="
  696. Loop
  697. On Error Resume Next
  698. Set xml = Server.CreateObject("MSXML2.DOMDocument.6.0")
  699. If Err.Number <> 0 Then
  700. Err.Clear
  701. Set xml = Server.CreateObject("MSXML2.DOMDocument")
  702. End If
  703. Set node = xml.createElement("base64")
  704. node.DataType = "bin.base64"
  705. node.Text = base64Value
  706. bytes = node.nodeTypedValue
  707. Set stream = Server.CreateObject("ADODB.Stream")
  708. stream.Type = 1
  709. stream.Open
  710. stream.Write bytes
  711. stream.Position = 0
  712. stream.Type = 2
  713. stream.Charset = "utf-8"
  714. Base64UrlDecodeToString = stream.ReadText
  715. stream.Close
  716. If Err.Number <> 0 Then
  717. ErrorMessage = "Unable to decode JWT payload: " & Err.Description
  718. Base64UrlDecodeToString = ""
  719. Call LogDiagnostic("ERROR", "jwt.decode_failed", ErrorMessage, "")
  720. Err.Clear
  721. End If
  722. Set stream = Nothing
  723. Set node = Nothing
  724. Set xml = Nothing
  725. On Error GoTo 0
  726. End Function
  727. Private Function NormalizeBaseUrl(ByVal value)
  728. value = Trim(CStr(value))
  729. Do While Right(value, 1) = "/"
  730. value = Left(value, Len(value) - 1)
  731. Loop
  732. NormalizeBaseUrl = value
  733. End Function
  734. Private Function CurrentRequestPathAndQuery()
  735. Dim requestPath, queryString
  736. CurrentRequestPathAndQuery = ""
  737. On Error Resume Next
  738. requestPath = Trim(CStr(Request.ServerVariables("HTTP_X_ORIGINAL_URL")))
  739. If Len(requestPath) = 0 Then requestPath = Trim(CStr(Request.ServerVariables("URL")))
  740. queryString = Trim(CStr(Request.ServerVariables("QUERY_STRING")))
  741. On Error GoTo 0
  742. If Len(requestPath) = 0 Then requestPath = "/"
  743. If Len(queryString) > 0 And InStr(1, requestPath, "?", vbBinaryCompare) = 0 Then
  744. requestPath = requestPath & "?" & queryString
  745. End If
  746. CurrentRequestPathAndQuery = requestPath
  747. End Function
  748. Private Function IsHttpsUrl(ByVal value)
  749. IsHttpsUrl = (LCase(Left(Trim(CStr(value)), 8)) = "https://")
  750. End Function
  751. Private Function UrlTargetsLocalhost(ByVal value)
  752. Dim normalizedValue
  753. normalizedValue = LCase(Trim(CStr(value)))
  754. UrlTargetsLocalhost = (InStr(1, normalizedValue, "://localhost", vbBinaryCompare) > 0 Or _
  755. InStr(1, normalizedValue, "://127.0.0.1", vbBinaryCompare) > 0 Or _
  756. InStr(1, normalizedValue, "://[::1]", vbBinaryCompare) > 0)
  757. End Function
  758. Private Function NestedDictionary(ByVal parentDictionary, ByVal key)
  759. Set NestedDictionary = Nothing
  760. If Not IsObject(parentDictionary) Then Exit Function
  761. If parentDictionary Is Nothing Then Exit Function
  762. If Not parentDictionary.Exists(key) Then Exit Function
  763. If Not IsObject(parentDictionary.Item(key)) Then Exit Function
  764. Set NestedDictionary = parentDictionary.Item(key)
  765. End Function
  766. Private Function CollectionContainsText(ByVal dictionary, ByVal expectedValue)
  767. Dim itemKey, itemValue
  768. CollectionContainsText = False
  769. If Len(Trim(CStr(expectedValue))) = 0 Then Exit Function
  770. If Not IsObject(dictionary) Then Exit Function
  771. If dictionary Is Nothing Then Exit Function
  772. For Each itemKey In dictionary.Keys
  773. itemValue = dictionary.Item(itemKey)
  774. If Not IsNull(itemValue) And Not IsEmpty(itemValue) Then
  775. If StrComp(CStr(itemValue), CStr(expectedValue), vbTextCompare) = 0 Then
  776. CollectionContainsText = True
  777. Exit Function
  778. End If
  779. End If
  780. Next
  781. End Function
  782. Private Function IsAuthPath(ByVal path)
  783. Dim normalizedPath
  784. normalizedPath = LCase(CStr(path))
  785. IsAuthPath = (Left(normalizedPath, 11) = "/auth/login" Or _
  786. Left(normalizedPath, 14) = "/auth/callback" Or _
  787. Left(normalizedPath, 12) = "/auth/logout")
  788. End Function
  789. Private Function AddFormValue(ByVal body, ByVal key, ByVal value)
  790. If Len(CStr(value)) = 0 Then
  791. AddFormValue = body
  792. Exit Function
  793. End If
  794. If Len(body) > 0 Then body = body & "&"
  795. AddFormValue = body & UrlEncode(key) & "=" & UrlEncode(value)
  796. End Function
  797. Private Function UrlEncode(ByVal value)
  798. UrlEncode = Server.URLEncode(CStr(value))
  799. End Function
  800. Private Function DictionaryString(ByVal dictionary, ByVal key)
  801. DictionaryString = ""
  802. If Not IsObject(dictionary) Then Exit Function
  803. If dictionary Is Nothing Then Exit Function
  804. If Not dictionary.Exists(key) Then Exit Function
  805. If IsNull(dictionary.Item(key)) Or IsEmpty(dictionary.Item(key)) Then Exit Function
  806. DictionaryString = CStr(dictionary.Item(key))
  807. End Function
  808. Private Function ReadSessionValue(ByVal key)
  809. Dim value
  810. ReadSessionValue = ""
  811. On Error Resume Next
  812. value = Session(key)
  813. If Err.Number <> 0 Then
  814. Err.Clear
  815. On Error GoTo 0
  816. Exit Function
  817. End If
  818. On Error GoTo 0
  819. If IsNull(value) Or IsEmpty(value) Then Exit Function
  820. ReadSessionValue = CStr(value)
  821. End Function
  822. Public Function EnsurePendingLoginValue(ByVal key)
  823. Dim value
  824. value = ReadPendingLoginValue(key)
  825. If Len(value) = 0 Then
  826. value = CreateRandomValue()
  827. Session(key) = value
  828. Call WritePendingLoginCookie(key, value)
  829. End If
  830. EnsurePendingLoginValue = value
  831. End Function
  832. Private Function ReadPendingLoginValue(ByVal key)
  833. ReadPendingLoginValue = ReadSessionValue(key)
  834. If Len(ReadPendingLoginValue) > 0 Then Exit Function
  835. ReadPendingLoginValue = ReadRequestCookie(key)
  836. End Function
  837. Private Sub ClearPendingLoginArtifacts()
  838. On Error Resume Next
  839. Session.Contents.Remove(SessionPrefix & "State")
  840. Session.Contents.Remove(SessionPrefix & "Nonce")
  841. Session.Contents.Remove(SessionPrefix & "LastIssuedState")
  842. Call ClearPendingLoginCookie(SessionPrefix & "PendingLogin")
  843. Call ClearPendingLoginCookie(SessionPrefix & "State")
  844. Call ClearPendingLoginCookie(SessionPrefix & "Nonce")
  845. Call ClearPendingLoginCookie(SessionPrefix & "LastIssuedState")
  846. Err.Clear
  847. On Error GoTo 0
  848. End Sub
  849. Private Function ReadRequestCookie(ByVal key)
  850. Dim cookieHeader, cookieParts, cookiePart, separatorPosition, currentName, currentValue
  851. ReadRequestCookie = ""
  852. If Len(Trim(CStr(key))) = 0 Then Exit Function
  853. On Error Resume Next
  854. cookieHeader = CStr(Request.ServerVariables("HTTP_COOKIE"))
  855. If Err.Number <> 0 Then
  856. Err.Clear
  857. On Error GoTo 0
  858. Exit Function
  859. End If
  860. On Error GoTo 0
  861. If Len(cookieHeader) = 0 Then Exit Function
  862. cookieParts = Split(cookieHeader, ";")
  863. For Each cookiePart In cookieParts
  864. separatorPosition = InStr(1, cookiePart, "=", vbBinaryCompare)
  865. If separatorPosition > 0 Then
  866. currentName = Trim(Left(cookiePart, separatorPosition - 1))
  867. currentName = DecodePercentEscapes(currentName)
  868. If StrComp(currentName, CStr(key), vbBinaryCompare) = 0 Then
  869. currentValue = Mid(cookiePart, separatorPosition + 1)
  870. ReadRequestCookie = Trim(DecodePercentEscapes(CStr(currentValue)))
  871. Exit Function
  872. End If
  873. End If
  874. Next
  875. End Function
  876. Private Function DecodePercentEscapes(ByVal value)
  877. Dim outputValue, position, currentChar, hexValue
  878. outputValue = ""
  879. position = 1
  880. Do While position <= Len(CStr(value))
  881. currentChar = Mid(CStr(value), position, 1)
  882. If currentChar = "%" And (position + 2) <= Len(CStr(value)) Then
  883. hexValue = Mid(CStr(value), position + 1, 2)
  884. If IsHexByte(hexValue) Then
  885. outputValue = outputValue & Chr(CLng("&H" & hexValue))
  886. position = position + 3
  887. Else
  888. outputValue = outputValue & currentChar
  889. position = position + 1
  890. End If
  891. Else
  892. outputValue = outputValue & currentChar
  893. position = position + 1
  894. End If
  895. Loop
  896. DecodePercentEscapes = outputValue
  897. End Function
  898. Private Function IsHexByte(ByVal value)
  899. Dim i, currentChar
  900. IsHexByte = False
  901. If Len(CStr(value)) <> 2 Then Exit Function
  902. For i = 1 To 2
  903. currentChar = UCase(Mid(CStr(value), i, 1))
  904. If InStr(1, "0123456789ABCDEF", currentChar, vbBinaryCompare) = 0 Then Exit Function
  905. Next
  906. IsHexByte = True
  907. End Function
  908. Private Sub WritePendingLoginCookie(ByVal key, ByVal value)
  909. If Len(Trim(CStr(key))) = 0 Then Exit Sub
  910. On Error Resume Next
  911. Response.Cookies(key) = CStr(value)
  912. Response.Cookies(key).Path = "/"
  913. Response.Cookies(key).Expires = DateAdd("n", CLng(PendingLoginCookieMinutes), Now())
  914. Response.Cookies(key).HttpOnly = True
  915. Err.Clear
  916. On Error GoTo 0
  917. End Sub
  918. Private Sub ClearPendingLoginCookie(ByVal key)
  919. If Len(Trim(CStr(key))) = 0 Then Exit Sub
  920. On Error Resume Next
  921. Response.Cookies(key) = ""
  922. Response.Cookies(key).Path = "/"
  923. Response.Cookies(key).Expires = DateAdd("d", -1, Now())
  924. Response.Cookies(key).HttpOnly = True
  925. Err.Clear
  926. On Error GoTo 0
  927. End Sub
  928. Private Function NormalizeStateValue(ByVal value)
  929. Dim textValue, i, currentChar, normalizedValue
  930. textValue = UCase(Trim(CStr(value)))
  931. normalizedValue = ""
  932. For i = 1 To Len(textValue)
  933. currentChar = Mid(textValue, i, 1)
  934. If (currentChar >= "A" And currentChar <= "Z") Or (currentChar >= "0" And currentChar <= "9") Then
  935. normalizedValue = normalizedValue & currentChar
  936. End If
  937. Next
  938. NormalizeStateValue = normalizedValue
  939. End Function
  940. Private Function StatesMatch(ByVal leftValue, ByVal rightValue)
  941. Dim normalizedLeftValue, normalizedRightValue
  942. normalizedLeftValue = NormalizeStateValue(leftValue)
  943. normalizedRightValue = NormalizeStateValue(rightValue)
  944. StatesMatch = (Len(normalizedLeftValue) > 0 And Len(normalizedRightValue) > 0 And normalizedLeftValue = normalizedRightValue)
  945. End Function
  946. Private Function IsSuccessStatus(ByVal statusCode)
  947. IsSuccessStatus = (CLng(statusCode) >= 200 And CLng(statusCode) < 300)
  948. End Function
  949. Private Function CreateHttpClient()
  950. Dim http
  951. Set CreateHttpClient = Nothing
  952. On Error Resume Next
  953. Set http = Server.CreateObject("MSXML2.ServerXMLHTTP.6.0")
  954. If Err.Number <> 0 Then
  955. Err.Clear
  956. Set http = Server.CreateObject("MSXML2.ServerXMLHTTP")
  957. End If
  958. If Err.Number <> 0 Then
  959. ErrorMessage = "Unable to create the Keycloak HTTP client: " & Err.Description
  960. Call LogDiagnostic("ERROR", "http.client_create_failed", ErrorMessage, "")
  961. Err.Clear
  962. Set http = Nothing
  963. Else
  964. Call ApplyHttpTimeouts(http)
  965. End If
  966. Set CreateHttpClient = http
  967. On Error GoTo 0
  968. End Function
  969. Private Sub ApplyHttpTimeouts(ByVal http)
  970. If Not IsObject(http) Then Exit Sub
  971. If http Is Nothing Then Exit Sub
  972. On Error Resume Next
  973. http.setTimeouts CLng(HttpResolveTimeoutMs), CLng(HttpConnectTimeoutMs), CLng(HttpSendTimeoutMs), CLng(HttpReceiveTimeoutMs)
  974. If Err.Number <> 0 Then
  975. Call LogDiagnostic("WARN", "http.timeout_apply_failed", "Unable to apply Keycloak HTTP timeouts.", "resolve_ms=" & CStr(HttpResolveTimeoutMs) & " connect_ms=" & CStr(HttpConnectTimeoutMs) & " send_ms=" & CStr(HttpSendTimeoutMs) & " receive_ms=" & CStr(HttpReceiveTimeoutMs))
  976. Err.Clear
  977. End If
  978. On Error GoTo 0
  979. End Sub
  980. Private Function AudienceContainsClientId(ByVal claims, audienceCount)
  981. Dim audienceValue, audienceCollection, audienceKey
  982. audienceCount = 0
  983. AudienceContainsClientId = False
  984. If Not IsObject(claims) Then Exit Function
  985. If claims Is Nothing Then Exit Function
  986. If Not claims.Exists("aud") Then Exit Function
  987. If IsObject(claims.Item("aud")) Then
  988. Set audienceCollection = claims.Item("aud")
  989. For Each audienceKey In audienceCollection.Keys
  990. audienceCount = audienceCount + 1
  991. If StrComp(CStr(audienceCollection.Item(audienceKey)), ClientId, vbBinaryCompare) = 0 Then
  992. AudienceContainsClientId = True
  993. End If
  994. Next
  995. Set audienceCollection = Nothing
  996. Exit Function
  997. End If
  998. audienceValue = claims.Item("aud")
  999. If IsNull(audienceValue) Or IsEmpty(audienceValue) Then Exit Function
  1000. audienceCount = 1
  1001. AudienceContainsClientId = (StrComp(CStr(audienceValue), ClientId, vbBinaryCompare) = 0)
  1002. End Function
  1003. Private Function TryGetNumericClaim(ByVal claims, ByVal key, numericValue)
  1004. Dim value
  1005. TryGetNumericClaim = False
  1006. numericValue = 0
  1007. If Not IsObject(claims) Then Exit Function
  1008. If claims Is Nothing Then Exit Function
  1009. If Not claims.Exists(key) Then Exit Function
  1010. value = claims.Item(key)
  1011. If IsNull(value) Or IsEmpty(value) Then Exit Function
  1012. If Not IsNumeric(value) Then Exit Function
  1013. numericValue = CLng(value)
  1014. TryGetNumericClaim = True
  1015. End Function
  1016. Private Function ClaimValueForLog(ByVal claims, ByVal key)
  1017. Dim value, claimCollection, claimKey, combinedValue
  1018. ClaimValueForLog = ""
  1019. If Not IsObject(claims) Then Exit Function
  1020. If claims Is Nothing Then Exit Function
  1021. If Not claims.Exists(key) Then Exit Function
  1022. If IsObject(claims.Item(key)) Then
  1023. Set claimCollection = claims.Item(key)
  1024. combinedValue = ""
  1025. For Each claimKey In claimCollection.Keys
  1026. If Len(combinedValue) > 0 Then combinedValue = combinedValue & ","
  1027. combinedValue = combinedValue & CStr(claimCollection.Item(claimKey))
  1028. Next
  1029. ClaimValueForLog = combinedValue
  1030. Set claimCollection = Nothing
  1031. Exit Function
  1032. End If
  1033. value = claims.Item(key)
  1034. If IsNull(value) Or IsEmpty(value) Then Exit Function
  1035. ClaimValueForLog = CStr(value)
  1036. End Function
  1037. Private Function CurrentUnixTime()
  1038. CurrentUnixTime = DateDiff("s", CDate("01/01/1970 00:00:00"), UtcNowValue())
  1039. End Function
  1040. Private Function UtcNowValue()
  1041. Dim biasMinutes, service, utcTimes, utcItem
  1042. UtcNowValue = Now()
  1043. If TryGetActiveUtcBiasMinutes(biasMinutes) Then
  1044. UtcNowValue = DateAdd("n", CLng(biasMinutes), Now())
  1045. Exit Function
  1046. End If
  1047. On Error Resume Next
  1048. Set service = GetObject("winmgmts:root\cimv2")
  1049. Set utcTimes = service.ExecQuery("SELECT * FROM Win32_UTCTime")
  1050. For Each utcItem In utcTimes
  1051. UtcNowValue = DateSerial(utcItem.Year, utcItem.Month, utcItem.Day) + TimeSerial(utcItem.Hour, utcItem.Minute, utcItem.Second)
  1052. Exit For
  1053. Next
  1054. Err.Clear
  1055. Set utcItem = Nothing
  1056. Set utcTimes = Nothing
  1057. Set service = Nothing
  1058. On Error GoTo 0
  1059. End Function
  1060. Private Function TryGetActiveUtcBiasMinutes(ByRef biasMinutes)
  1061. Dim shell, value
  1062. TryGetActiveUtcBiasMinutes = False
  1063. biasMinutes = 0
  1064. On Error Resume Next
  1065. Set shell = Server.CreateObject("WScript.Shell")
  1066. value = shell.RegRead("HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias")
  1067. If Err.Number = 0 Then
  1068. biasMinutes = CLng(value)
  1069. TryGetActiveUtcBiasMinutes = True
  1070. Else
  1071. Err.Clear
  1072. End If
  1073. Set shell = Nothing
  1074. On Error GoTo 0
  1075. End Function
  1076. Private Function BuildHttpDetail(ByVal method, ByVal url, ByVal statusCode, ByVal responseText)
  1077. BuildHttpDetail = "method=" & SafeLogValue(method) & " url=" & SafeLogValue(url) & " status=" & CStr(statusCode) & " resolve_ms=" & CStr(HttpResolveTimeoutMs) & " connect_ms=" & CStr(HttpConnectTimeoutMs) & " send_ms=" & CStr(HttpSendTimeoutMs) & " receive_ms=" & CStr(HttpReceiveTimeoutMs)
  1078. If Len(responseText) > 0 Then
  1079. BuildHttpDetail = BuildHttpDetail & " response_length=" & CStr(Len(CStr(responseText)))
  1080. End If
  1081. End Function
  1082. Private Sub LogDiagnostic(ByVal level, ByVal eventName, ByVal message, ByVal detail)
  1083. Dim logEntry, appendValue
  1084. logEntry = "[" & Replace(CStr(UtcNowValue()), vbCrLf, " ") & "] [Keycloak] level=" & UCase(CStr(level)) & " event=" & SafeLogValue(eventName) & " client_id=" & SafeLogValue(ClientId) & " realm=" & SafeLogValue(Realm) & " message=" & SafeLogValue(message)
  1085. If Len(detail) > 0 Then logEntry = logEntry & " detail=" & SafeLogValue(detail)
  1086. appendValue = "kc " & Left(CStr(eventName) & " " & TruncateForLog(message, 48), 80)
  1087. On Error Resume Next
  1088. Response.AppendToLog appendValue
  1089. If EnableDiagnosticLogging And Len(DiagnosticLogPath) > 0 Then
  1090. Call AppendLogFile(logEntry)
  1091. End If
  1092. Err.Clear
  1093. On Error GoTo 0
  1094. End Sub
  1095. Private Sub AppendLogFile(ByVal logEntry)
  1096. Dim fso, logFile
  1097. If Len(DiagnosticLogPath) = 0 Then Exit Sub
  1098. On Error Resume Next
  1099. Set fso = Server.CreateObject("Scripting.FileSystemObject")
  1100. If fso.FileExists(DiagnosticLogPath) Then
  1101. Set logFile = fso.OpenTextFile(DiagnosticLogPath, 8, False)
  1102. Else
  1103. Set logFile = fso.CreateTextFile(DiagnosticLogPath, True)
  1104. End If
  1105. logFile.WriteLine logEntry
  1106. logFile.Close
  1107. Set logFile = Nothing
  1108. Set fso = Nothing
  1109. Err.Clear
  1110. On Error GoTo 0
  1111. End Sub
  1112. Private Function TruncateForLog(ByVal value, ByVal maxLength)
  1113. Dim textValue
  1114. textValue = CStr(value)
  1115. If Len(textValue) <= CLng(maxLength) Then
  1116. TruncateForLog = textValue
  1117. Else
  1118. TruncateForLog = Left(textValue, CLng(maxLength) - 3) & "..."
  1119. End If
  1120. End Function
  1121. Private Function SafeLogValue(ByVal value)
  1122. Dim textValue
  1123. textValue = Replace(CStr(value), vbCr, " ")
  1124. textValue = Replace(textValue, vbLf, " ")
  1125. textValue = Replace(textValue, "|", "/")
  1126. SafeLogValue = Trim(textValue)
  1127. End Function
  1128. Private Function SensitiveValueSummary(ByVal value)
  1129. SensitiveValueSummary = "len=" & CStr(Len(CStr(value)))
  1130. End Function
  1131. Private Function CreateRandomValue()
  1132. Dim guidProvider, rawValue
  1133. Randomize
  1134. rawValue = ""
  1135. On Error Resume Next
  1136. Set guidProvider = Server.CreateObject("Scriptlet.TypeLib")
  1137. If Err.Number = 0 Then rawValue = guidProvider.Guid
  1138. If Err.Number <> 0 Or Len(rawValue) = 0 Then
  1139. Err.Clear
  1140. rawValue = CStr(Now()) & "-" & CStr(Timer) & "-" & CStr(Rnd())
  1141. End If
  1142. On Error GoTo 0
  1143. rawValue = Replace(rawValue, "{", "")
  1144. rawValue = Replace(rawValue, "}", "")
  1145. rawValue = Replace(rawValue, "-", "")
  1146. rawValue = Replace(rawValue, " ", "")
  1147. rawValue = Replace(rawValue, ":", "")
  1148. rawValue = Replace(rawValue, "/", "")
  1149. rawValue = Replace(rawValue, ".", "")
  1150. CreateRandomValue = rawValue
  1151. End Function
  1152. End Class
  1153. Function KeycloakReadAppSetting(ByVal key, ByVal fallbackValue)
  1154. Dim value
  1155. KeycloakReadAppSetting = fallbackValue
  1156. On Error Resume Next
  1157. value = GetAppSetting(key)
  1158. If Err.Number <> 0 Then
  1159. Err.Clear
  1160. On Error GoTo 0
  1161. Exit Function
  1162. End If
  1163. On Error GoTo 0
  1164. If IsNull(value) Or IsEmpty(value) Then Exit Function
  1165. If Len(CStr(value)) = 0 Then Exit Function
  1166. If LCase(CStr(value)) = "nothing" Then Exit Function
  1167. KeycloakReadAppSetting = CStr(value)
  1168. End Function
  1169. Function KeycloakReadAppSettingBool(ByVal key, ByVal fallbackValue)
  1170. Dim value
  1171. KeycloakReadAppSettingBool = CBool(fallbackValue)
  1172. value = LCase(Trim(CStr(KeycloakReadAppSetting(key, ""))))
  1173. If Len(value) = 0 Then Exit Function
  1174. Select Case value
  1175. Case "true", "1", "yes", "on"
  1176. KeycloakReadAppSettingBool = True
  1177. Case "false", "0", "no", "off"
  1178. KeycloakReadAppSettingBool = False
  1179. End Select
  1180. End Function
  1181. Function KeycloakReadAppSettingLong(ByVal key, ByVal fallbackValue)
  1182. Dim value
  1183. KeycloakReadAppSettingLong = CLng(fallbackValue)
  1184. value = KeycloakReadAppSetting(key, "")
  1185. If Len(Trim(CStr(value))) = 0 Then Exit Function
  1186. If Not IsNumeric(value) Then Exit Function
  1187. KeycloakReadAppSettingLong = CLng(value)
  1188. End Function
  1189. Dim KeycloakAuth_Class__Singleton
  1190. Function KeycloakAuth()
  1191. If IsEmpty(KeycloakAuth_Class__Singleton) Then
  1192. Set KeycloakAuth_Class__Singleton = New KeycloakAuth_Class
  1193. Call KeycloakAuth_Class__Singleton.ConfigureFromAppSettings()
  1194. End If
  1195. Set KeycloakAuth = KeycloakAuth_Class__Singleton
  1196. End Function
  1197. Function Keycloak()
  1198. Set Keycloak = KeycloakAuth()
  1199. End Function
  1200. Sub KeycloakLogin()
  1201. Dim auth
  1202. Set auth = KeycloakAuth()
  1203. Call auth.Login()
  1204. End Sub
  1205. Function KeycloakHandleCallback()
  1206. Dim auth
  1207. Set auth = KeycloakAuth()
  1208. KeycloakHandleCallback = auth.HandleCallback()
  1209. End Function
  1210. Function KeycloakIsLoggedIn()
  1211. Dim auth
  1212. Set auth = KeycloakAuth()
  1213. KeycloakIsLoggedIn = auth.IsLoggedIn()
  1214. End Function
  1215. Function KeycloakAccessToken()
  1216. Dim auth
  1217. Set auth = KeycloakAuth()
  1218. KeycloakAccessToken = auth.GetSessionAccessToken()
  1219. End Function
  1220. Function KeycloakRefreshToken()
  1221. Dim auth
  1222. Set auth = KeycloakAuth()
  1223. KeycloakRefreshToken = auth.GetSessionRefreshToken()
  1224. End Function
  1225. Function KeycloakIdToken()
  1226. Dim auth
  1227. Set auth = KeycloakAuth()
  1228. KeycloakIdToken = auth.GetSessionIdToken()
  1229. End Function
  1230. Function KeycloakCurrentUser()
  1231. Dim auth
  1232. Set auth = KeycloakAuth()
  1233. Set KeycloakCurrentUser = auth.CurrentUser()
  1234. End Function
  1235. Function KeycloakUserInfo()
  1236. Dim auth
  1237. Set auth = KeycloakAuth()
  1238. Set KeycloakUserInfo = auth.GetUserInfo("")
  1239. End Function
  1240. Function KeycloakTokenClaims(ByVal token)
  1241. Dim auth
  1242. Set auth = KeycloakAuth()
  1243. Set KeycloakTokenClaims = auth.GetTokenClaims(token)
  1244. End Function
  1245. Function KeycloakRequireLogin(ByVal returnToPath)
  1246. Dim auth
  1247. Set auth = KeycloakAuth()
  1248. KeycloakRequireLogin = auth.RequireLogin(returnToPath)
  1249. End Function
  1250. Sub KeycloakSetPostLoginRedirectPath(ByVal returnToPath)
  1251. Dim auth
  1252. Set auth = KeycloakAuth()
  1253. Call auth.SetPostLoginRedirectPath(returnToPath)
  1254. End Sub
  1255. Function KeycloakConsumePostLoginRedirectPath(ByVal fallbackPath)
  1256. Dim auth
  1257. Set auth = KeycloakAuth()
  1258. KeycloakConsumePostLoginRedirectPath = auth.ConsumePostLoginRedirectPath(fallbackPath)
  1259. End Function
  1260. Function KeycloakHasRealmRole(ByVal roleName)
  1261. Dim auth
  1262. Set auth = KeycloakAuth()
  1263. KeycloakHasRealmRole = auth.HasRealmRole(roleName)
  1264. End Function
  1265. Function KeycloakHasClientRole(ByVal clientId, ByVal roleName)
  1266. Dim auth
  1267. Set auth = KeycloakAuth()
  1268. KeycloakHasClientRole = auth.HasClientRole(clientId, roleName)
  1269. End Function
  1270. Function KeycloakLogoutUrl(ByVal postLogoutRedirectUri)
  1271. Dim auth
  1272. Set auth = KeycloakAuth()
  1273. KeycloakLogoutUrl = auth.BuildLogoutUrl(postLogoutRedirectUri)
  1274. End Function
  1275. Sub KeycloakLogout(ByVal postLogoutRedirectUri)
  1276. Dim auth
  1277. Set auth = KeycloakAuth()
  1278. Call auth.Logout(postLogoutRedirectUri)
  1279. End Sub
  1280. %>

Powered by TurnKey Linux.