Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

289 lines
14KB

  1. <!-- #include file="../aspunit/Lib/ASPUnit.asp" -->
  2. <!-- #include file="../bootstrap.asp" -->
  3. <!-- #include file="../../core/lib.json.asp" -->
  4. <!-- #include file="../../core/lib.Keycloak.asp" -->
  5. <%
  6. Call ASPUnit.AddModule( _
  7. ASPUnit.CreateModule( _
  8. "Keycloak Auth Tests", _
  9. Array( _
  10. ASPUnit.CreateTest("KeycloakEndpointsUseRealmBaseUrl"), _
  11. ASPUnit.CreateTest("KeycloakBuildLoginUrlIncludesOidcParameters"), _
  12. ASPUnit.CreateTest("KeycloakBuildLogoutUrlIncludesClientAndRedirect"), _
  13. ASPUnit.CreateTest("KeycloakTokenClaimsDecodeJwtPayload"), _
  14. ASPUnit.CreateTest("KeycloakAuthDefaultsHttpTimeoutsAndClockSkew"), _
  15. ASPUnit.CreateTest("KeycloakOperationalConfigurationAllowsProductionSafeHttpsUrls"), _
  16. ASPUnit.CreateTest("KeycloakOperationalConfigurationRejectsProductionHttpRedirectUri"), _
  17. ASPUnit.CreateTest("KeycloakOperationalConfigurationAllowsLocalHttpDuringDevelopment"), _
  18. ASPUnit.CreateTest("KeycloakSetPostLoginRedirectPathStoresRelativePath"), _
  19. ASPUnit.CreateTest("KeycloakSetPostLoginRedirectPathRejectsAbsoluteUrl"), _
  20. ASPUnit.CreateTest("KeycloakConsumePostLoginRedirectPathReturnsStoredValueAndClearsIt"), _
  21. ASPUnit.CreateTest("KeycloakHasRealmRoleReadsIdTokenClaims"), _
  22. ASPUnit.CreateTest("KeycloakHasClientRoleReadsIdTokenClaims"), _
  23. ASPUnit.CreateTest("KeycloakValidateIdTokenAcceptsExpectedClaims"), _
  24. ASPUnit.CreateTest("KeycloakValidateIdTokenRejectsNonceMismatch"), _
  25. ASPUnit.CreateTest("KeycloakValidateIdTokenRejectsExpiredTokens"), _
  26. ASPUnit.CreateTest("KeycloakValidateIdTokenAcceptsMultipleAudiencesWhenAzpMatches") _
  27. ), _
  28. ASPUnit.CreateLifeCycle("SetupKeycloakAuth", "TeardownKeycloakAuth") _
  29. ) _
  30. )
  31. Call ASPUnit.Run()
  32. Sub SetupKeycloakAuth()
  33. Call ResetTestRuntime()
  34. KeycloakAuth_Class__Singleton = Empty
  35. Session.Contents.Remove "Keycloak_IdToken"
  36. Session.Contents.Remove "Keycloak_PostLoginRedirectPath"
  37. End Sub
  38. Sub TeardownKeycloakAuth()
  39. Session.Contents.Remove "Keycloak_IdToken"
  40. Session.Contents.Remove "Keycloak_PostLoginRedirectPath"
  41. KeycloakAuth_Class__Singleton = Empty
  42. Call ResetTestRuntime()
  43. End Sub
  44. Function NewTestKeycloakAuth()
  45. Dim auth
  46. Set auth = New KeycloakAuth_Class
  47. Call auth.Configure("https://login.example.test/", "survey", "classic-app", "secret", "https://app.example.test/auth/callback")
  48. auth.LogoutRedirectUri = "https://app.example.test/"
  49. Set NewTestKeycloakAuth = auth
  50. End Function
  51. Function KeycloakEndpointsUseRealmBaseUrl()
  52. Dim auth
  53. Set auth = NewTestKeycloakAuth()
  54. Call ASPUnit.Equal(auth.RealmBaseUrl(), "https://login.example.test/realms/survey", "RealmBaseUrl should trim a trailing Keycloak base URL slash")
  55. Call ASPUnit.Equal(auth.AuthorizationEndpoint(), "https://login.example.test/realms/survey/protocol/openid-connect/auth", "AuthorizationEndpoint should use the realm OIDC auth endpoint")
  56. Call ASPUnit.Equal(auth.TokenEndpoint(), "https://login.example.test/realms/survey/protocol/openid-connect/token", "TokenEndpoint should use the realm OIDC token endpoint")
  57. Call ASPUnit.Equal(auth.UserInfoEndpoint(), "https://login.example.test/realms/survey/protocol/openid-connect/userinfo", "UserInfoEndpoint should use the realm OIDC userinfo endpoint")
  58. End Function
  59. Function KeycloakBuildLoginUrlIncludesOidcParameters()
  60. Dim auth, loginUrl, expectedUrl
  61. Set auth = NewTestKeycloakAuth()
  62. loginUrl = auth.BuildLoginUrl("state-123", "nonce-456")
  63. expectedUrl = "https://login.example.test/realms/survey/protocol/openid-connect/auth?client_id=classic%2Dapp&response_type=code&scope=openid+profile+email&redirect_uri=https%3A%2F%2Fapp%2Eexample%2Etest%2Fauth%2Fcallback&state=state%2D123&nonce=nonce%2D456"
  64. Call ASPUnit.Equal(loginUrl, expectedUrl, "BuildLoginUrl should include the encoded OIDC authorization-code parameters")
  65. End Function
  66. Function KeycloakBuildLogoutUrlIncludesClientAndRedirect()
  67. Dim auth, logoutUrl, expectedUrl
  68. Set auth = NewTestKeycloakAuth()
  69. logoutUrl = auth.BuildLogoutUrl("")
  70. expectedUrl = "https://login.example.test/realms/survey/protocol/openid-connect/logout?client_id=classic%2Dapp&post_logout_redirect_uri=https%3A%2F%2Fapp%2Eexample%2Etest%2F"
  71. Call ASPUnit.Equal(logoutUrl, expectedUrl, "BuildLogoutUrl should include the encoded client id and post-logout redirect URI")
  72. End Function
  73. Function KeycloakTokenClaimsDecodeJwtPayload()
  74. Dim auth, token, claims
  75. Set auth = NewTestKeycloakAuth()
  76. token = "e30.eyJzdWIiOiJ1c2VyLTEyMyIsInByZWZlcnJlZF91c2VybmFtZSI6ImRhbmEiLCJlbWFpbCI6ImRhbmFAZXhhbXBsZS50ZXN0In0.signature"
  77. Set claims = auth.GetTokenClaims(token)
  78. Call ASPUnit.Ok(Not claims Is Nothing, "GetTokenClaims should return a dictionary for a valid JWT")
  79. Call ASPUnit.Equal(claims.Item("sub"), "user-123", "GetTokenClaims should decode the sub claim")
  80. Call ASPUnit.Equal(claims.Item("preferred_username"), "dana", "GetTokenClaims should decode the preferred username")
  81. Call ASPUnit.Equal(claims.Item("email"), "dana@example.test", "GetTokenClaims should decode the email")
  82. End Function
  83. Function KeycloakAuthDefaultsHttpTimeoutsAndClockSkew()
  84. Dim auth
  85. Set auth = NewTestKeycloakAuth()
  86. Call ASPUnit.Ok(auth.HttpResolveTimeoutMs > 0, "KeycloakAuth should default the DNS resolve timeout to a positive value")
  87. Call ASPUnit.Ok(auth.HttpConnectTimeoutMs > 0, "KeycloakAuth should default the connect timeout to a positive value")
  88. Call ASPUnit.Ok(auth.HttpSendTimeoutMs > 0, "KeycloakAuth should default the send timeout to a positive value")
  89. Call ASPUnit.Ok(auth.HttpReceiveTimeoutMs > 0, "KeycloakAuth should default the receive timeout to a positive value")
  90. Call ASPUnit.Ok(auth.AllowedClockSkewSeconds >= 0, "KeycloakAuth should default the allowed clock skew to a non-negative value")
  91. End Function
  92. Function KeycloakOperationalConfigurationAllowsProductionSafeHttpsUrls()
  93. Dim auth, result
  94. Set auth = NewTestKeycloakAuth()
  95. result = auth.ValidateOperationalConfiguration("Production")
  96. Call ASPUnit.Ok(CBool(result), "ValidateOperationalConfiguration should accept HTTPS Keycloak URLs in Production")
  97. Call ASPUnit.Equal(auth.ErrorMessage, "", "ValidateOperationalConfiguration should not leave an error message for production-safe URLs")
  98. End Function
  99. Function KeycloakOperationalConfigurationRejectsProductionHttpRedirectUri()
  100. Dim auth, result
  101. Set auth = NewTestKeycloakAuth()
  102. auth.RedirectUri = "http://app.example.test/auth/callback"
  103. result = auth.ValidateOperationalConfiguration("Production")
  104. Call ASPUnit.Ok(Not CBool(result), "ValidateOperationalConfiguration should reject a non-HTTPS redirect URI in Production")
  105. Call ASPUnit.Ok(InStr(LCase(auth.ErrorMessage), "https") > 0 And InStr(LCase(auth.ErrorMessage), "redirecturi") > 0, "ValidateOperationalConfiguration should explain redirect URI HTTPS failures")
  106. End Function
  107. Function KeycloakOperationalConfigurationAllowsLocalHttpDuringDevelopment()
  108. Dim auth, result
  109. Set auth = NewTestKeycloakAuth()
  110. auth.BaseUrl = "http://localhost:8180"
  111. auth.RedirectUri = "http://localhost:8080/auth/callback"
  112. auth.LogoutRedirectUri = "http://localhost:8080/"
  113. result = auth.ValidateOperationalConfiguration("Development")
  114. Call ASPUnit.Ok(CBool(result), "ValidateOperationalConfiguration should allow local HTTP URLs outside Production")
  115. End Function
  116. Function KeycloakSetPostLoginRedirectPathStoresRelativePath()
  117. Dim auth
  118. Set auth = NewTestKeycloakAuth()
  119. Call auth.SetPostLoginRedirectPath("/reports/weekly?site=42")
  120. Call ASPUnit.Equal(Session("Keycloak_PostLoginRedirectPath"), "/reports/weekly?site=42", "SetPostLoginRedirectPath should store a safe relative return path in Session")
  121. End Function
  122. Function KeycloakSetPostLoginRedirectPathRejectsAbsoluteUrl()
  123. Dim auth, storedValue
  124. Set auth = NewTestKeycloakAuth()
  125. Call auth.SetPostLoginRedirectPath("https://evil.example.test/phish")
  126. On Error Resume Next
  127. storedValue = Session("Keycloak_PostLoginRedirectPath")
  128. If Err.Number <> 0 Then
  129. storedValue = ""
  130. Err.Clear
  131. End If
  132. On Error GoTo 0
  133. Call ASPUnit.Equal(CStr(storedValue), "", "SetPostLoginRedirectPath should ignore absolute URLs to avoid open redirects")
  134. End Function
  135. Function KeycloakConsumePostLoginRedirectPathReturnsStoredValueAndClearsIt()
  136. Dim auth, redirectPath, storedValue
  137. Set auth = NewTestKeycloakAuth()
  138. Session("Keycloak_PostLoginRedirectPath") = "/surveys/open/7"
  139. redirectPath = auth.ConsumePostLoginRedirectPath("/")
  140. On Error Resume Next
  141. storedValue = Session("Keycloak_PostLoginRedirectPath")
  142. If Err.Number <> 0 Then
  143. storedValue = ""
  144. Err.Clear
  145. End If
  146. On Error GoTo 0
  147. Call ASPUnit.Equal(redirectPath, "/surveys/open/7", "ConsumePostLoginRedirectPath should return the stored post-login destination")
  148. Call ASPUnit.Equal(CStr(storedValue), "", "ConsumePostLoginRedirectPath should clear the stored post-login destination after reading it")
  149. End Function
  150. Function KeycloakHasRealmRoleReadsIdTokenClaims()
  151. Dim auth
  152. Set auth = NewTestKeycloakAuth()
  153. Session("Keycloak_IdToken") = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":""classic-app"",""exp"":2147483647,""iat"":1700000000,""realm_access"":{""roles"":[""admin"",""author""]}}")
  154. Call ASPUnit.Ok(auth.HasRealmRole("author"), "HasRealmRole should read realm roles from the stored ID token claims")
  155. Call ASPUnit.Ok(Not auth.HasRealmRole("approver"), "HasRealmRole should return False when the requested realm role is missing")
  156. End Function
  157. Function KeycloakHasClientRoleReadsIdTokenClaims()
  158. Dim auth
  159. Set auth = NewTestKeycloakAuth()
  160. Session("Keycloak_IdToken") = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":""classic-app"",""exp"":2147483647,""iat"":1700000000,""resource_access"":{""classic-app"":{""roles"":[""editor"",""reviewer""]}}}")
  161. Call ASPUnit.Ok(auth.HasClientRole("classic-app", "reviewer"), "HasClientRole should read client roles from the stored ID token claims")
  162. Call ASPUnit.Ok(Not auth.HasClientRole("classic-app", "publisher"), "HasClientRole should return False when the requested client role is missing")
  163. End Function
  164. Function KeycloakValidateIdTokenAcceptsExpectedClaims()
  165. Dim auth, token, result
  166. Set auth = NewTestKeycloakAuth()
  167. token = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":""classic-app"",""exp"":2147483647,""iat"":1700000000,""nonce"":""nonce-456""}")
  168. result = auth.ValidateIdToken(token, "nonce-456", True)
  169. Call ASPUnit.Ok(CBool(result), "ValidateIdToken should accept a token whose issuer, audience, expiry, and nonce all match")
  170. Call ASPUnit.Equal(auth.ErrorMessage, "", "ValidateIdToken should not leave an error message behind for a valid token")
  171. End Function
  172. Function KeycloakValidateIdTokenRejectsNonceMismatch()
  173. Dim auth, token, result
  174. Set auth = NewTestKeycloakAuth()
  175. token = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":""classic-app"",""exp"":2147483647,""iat"":1700000000,""nonce"":""unexpected-nonce""}")
  176. result = auth.ValidateIdToken(token, "expected-nonce", True)
  177. Call ASPUnit.Ok(Not CBool(result), "ValidateIdToken should reject a token when the nonce claim differs from the login session nonce")
  178. Call ASPUnit.Ok(InStr(LCase(auth.ErrorMessage), "nonce") > 0, "ValidateIdToken should mention the nonce in the failure message when the nonce is wrong")
  179. End Function
  180. Function KeycloakValidateIdTokenRejectsExpiredTokens()
  181. Dim auth, token, result
  182. Set auth = NewTestKeycloakAuth()
  183. auth.AllowedClockSkewSeconds = 0
  184. token = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":""classic-app"",""exp"":946684800,""iat"":946684200,""nonce"":""nonce-456""}")
  185. result = auth.ValidateIdToken(token, "nonce-456", True)
  186. Call ASPUnit.Ok(Not CBool(result), "ValidateIdToken should reject an ID token whose exp claim is already in the past")
  187. Call ASPUnit.Ok(InStr(LCase(auth.ErrorMessage), "expired") > 0, "ValidateIdToken should mention expiration when the token has expired")
  188. End Function
  189. Function KeycloakValidateIdTokenAcceptsMultipleAudiencesWhenAzpMatches()
  190. Dim auth, token, result
  191. Set auth = NewTestKeycloakAuth()
  192. token = BuildUnsignedJwt("{""iss"":""" & auth.RealmBaseUrl() & """,""sub"":""user-123"",""aud"":[""account"",""classic-app""],""azp"":""classic-app"",""exp"":2147483647,""iat"":1700000000,""nonce"":""nonce-456""}")
  193. result = auth.ValidateIdToken(token, "nonce-456", True)
  194. Call ASPUnit.Ok(CBool(result), "ValidateIdToken should accept a multi-audience ID token when azp matches the configured client")
  195. End Function
  196. Function BuildUnsignedJwt(ByVal payloadJson)
  197. Dim xml, node, stream, bytes, base64Value
  198. Set stream = Server.CreateObject("ADODB.Stream")
  199. stream.Type = 2
  200. stream.Charset = "utf-8"
  201. stream.Open
  202. stream.WriteText payloadJson
  203. stream.Position = 0
  204. stream.Type = 1
  205. bytes = stream.Read
  206. stream.Close
  207. On Error Resume Next
  208. Set xml = Server.CreateObject("MSXML2.DOMDocument.6.0")
  209. If Err.Number <> 0 Then
  210. Err.Clear
  211. Set xml = Server.CreateObject("MSXML2.DOMDocument")
  212. End If
  213. On Error GoTo 0
  214. Set node = xml.createElement("base64")
  215. node.DataType = "bin.base64"
  216. node.nodeTypedValue = bytes
  217. base64Value = Replace(Replace(node.Text, vbCr, ""), vbLf, "")
  218. base64Value = Replace(base64Value, "+", "-")
  219. base64Value = Replace(base64Value, "/", "_")
  220. base64Value = Replace(base64Value, "=", "")
  221. BuildUnsignedJwt = "e30." & base64Value & ".signature"
  222. Set node = Nothing
  223. Set xml = Nothing
  224. Set stream = Nothing
  225. End Function
  226. %>

Powered by TurnKey Linux.