|
- <%
- '==============================================================================
- ' Keycloak OpenID Connect helper
- '==============================================================================
- ' Typical flow:
- ' 1. Configure KeycloakBaseUrl/Realm/ClientId/ClientSecret/RedirectUri in
- ' public/web.config, or set those properties on KeycloakAuth().
- ' 2. Send users to KeycloakLogin().
- ' 3. Point the Keycloak redirect URI at an action that calls
- ' KeycloakHandleCallback().
- ' 4. Use KeycloakCurrentUser(), KeycloakAccessToken(), or KeycloakUserInfo().
- '==============================================================================
-
- Class KeycloakAuth_Class
- Public BaseUrl
- Public Realm
- Public ClientId
- Public ClientSecret
- Public RedirectUri
- Public Scope
- Public LogoutRedirectUri
- Public SessionPrefix
- Public PendingLoginCookieMinutes
- Public AllowedClockSkewSeconds
- Public HttpResolveTimeoutMs
- Public HttpConnectTimeoutMs
- Public HttpSendTimeoutMs
- Public HttpReceiveTimeoutMs
- Public EnableDiagnosticLogging
- Public DiagnosticLogPath
- Public ErrorMessage
- Public LastStatus
- Public LastResponseText
- Public TokenResponse
- Public UserInfo
-
- Private Sub Class_Initialize()
- BaseUrl = ""
- Realm = ""
- ClientId = ""
- ClientSecret = ""
- RedirectUri = ""
- Scope = "openid profile email"
- LogoutRedirectUri = ""
- SessionPrefix = "Keycloak_"
- PendingLoginCookieMinutes = 15
- AllowedClockSkewSeconds = 300
- HttpResolveTimeoutMs = 5000
- HttpConnectTimeoutMs = 5000
- HttpSendTimeoutMs = 15000
- HttpReceiveTimeoutMs = 15000
- EnableDiagnosticLogging = False
- DiagnosticLogPath = ""
- ErrorMessage = ""
- LastStatus = 0
- LastResponseText = ""
- Set TokenResponse = Nothing
- Set UserInfo = Nothing
- End Sub
-
- Public Sub Configure(ByVal keycloakBaseUrl, ByVal keycloakRealm, ByVal keycloakClientId, ByVal keycloakClientSecret, ByVal keycloakRedirectUri)
- BaseUrl = Trim(CStr(keycloakBaseUrl))
- Realm = Trim(CStr(keycloakRealm))
- ClientId = Trim(CStr(keycloakClientId))
- ClientSecret = CStr(keycloakClientSecret)
- RedirectUri = Trim(CStr(keycloakRedirectUri))
- End Sub
-
- Public Sub ConfigureFromAppSettings()
- Dim sharedHttpTimeoutMs
-
- BaseUrl = KeycloakReadAppSetting("KeycloakBaseUrl", BaseUrl)
- Realm = KeycloakReadAppSetting("KeycloakRealm", Realm)
- ClientId = KeycloakReadAppSetting("KeycloakClientId", ClientId)
- ClientSecret = KeycloakReadAppSetting("KeycloakClientSecret", ClientSecret)
- RedirectUri = KeycloakReadAppSetting("KeycloakRedirectUri", RedirectUri)
- Scope = KeycloakReadAppSetting("KeycloakScope", Scope)
- LogoutRedirectUri = KeycloakReadAppSetting("KeycloakLogoutRedirectUri", LogoutRedirectUri)
- PendingLoginCookieMinutes = KeycloakReadAppSettingLong("KeycloakPendingLoginCookieMinutes", PendingLoginCookieMinutes)
- AllowedClockSkewSeconds = KeycloakReadAppSettingLong("KeycloakAllowedClockSkewSeconds", AllowedClockSkewSeconds)
-
- sharedHttpTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpTimeoutMs", 0)
- If sharedHttpTimeoutMs > 0 Then
- HttpResolveTimeoutMs = sharedHttpTimeoutMs
- HttpConnectTimeoutMs = sharedHttpTimeoutMs
- HttpSendTimeoutMs = sharedHttpTimeoutMs
- HttpReceiveTimeoutMs = sharedHttpTimeoutMs
- End If
-
- HttpResolveTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpResolveTimeoutMs", HttpResolveTimeoutMs)
- HttpConnectTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpConnectTimeoutMs", HttpConnectTimeoutMs)
- HttpSendTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpSendTimeoutMs", HttpSendTimeoutMs)
- HttpReceiveTimeoutMs = KeycloakReadAppSettingLong("KeycloakHttpReceiveTimeoutMs", HttpReceiveTimeoutMs)
-
- EnableDiagnosticLogging = KeycloakReadAppSettingBool("KeycloakEnableLogging", KeycloakReadAppSettingBool("EnableErrorLogging", EnableDiagnosticLogging))
- DiagnosticLogPath = KeycloakReadAppSetting("KeycloakLogPath", KeycloakReadAppSetting("ErrorLogPath", DiagnosticLogPath))
- End Sub
-
- Public Function IsConfigured()
- IsConfigured = (Len(NormalizeBaseUrl(BaseUrl)) > 0 And Len(Realm) > 0 And Len(ClientId) > 0 And Len(RedirectUri) > 0)
- End Function
-
- Public Function ValidateOperationalConfiguration(ByVal environmentName)
- Dim normalizedEnvironment
-
- ValidateOperationalConfiguration = True
- ErrorMessage = ""
- normalizedEnvironment = LCase(Trim(CStr(environmentName)))
-
- If normalizedEnvironment <> "production" Then Exit Function
-
- If Not IsHttpsUrl(BaseUrl) Then
- ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakBaseUrl."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
-
- If UrlTargetsLocalhost(BaseUrl) Then
- ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakBaseUrl."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
-
- If Not IsHttpsUrl(RedirectUri) Then
- ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakRedirectUri."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
-
- If UrlTargetsLocalhost(RedirectUri) Then
- ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakRedirectUri."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
-
- If Len(Trim(CStr(LogoutRedirectUri))) > 0 Then
- If Not IsHttpsUrl(LogoutRedirectUri) Then
- ErrorMessage = "Production Keycloak configuration requires an HTTPS KeycloakLogoutRedirectUri."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
-
- If UrlTargetsLocalhost(LogoutRedirectUri) Then
- ErrorMessage = "Production Keycloak configuration cannot use a localhost KeycloakLogoutRedirectUri."
- ValidateOperationalConfiguration = False
- Exit Function
- End If
- End If
- End Function
-
- Public Function RealmBaseUrl()
- If Len(NormalizeBaseUrl(BaseUrl)) = 0 Or Len(Realm) = 0 Then
- RealmBaseUrl = ""
- Exit Function
- End If
-
- RealmBaseUrl = NormalizeBaseUrl(BaseUrl) & "/realms/" & Realm
- End Function
-
- Public Function AuthorizationEndpoint()
- AuthorizationEndpoint = RealmBaseUrl() & "/protocol/openid-connect/auth"
- End Function
-
- Public Function TokenEndpoint()
- TokenEndpoint = RealmBaseUrl() & "/protocol/openid-connect/token"
- End Function
-
- Public Function UserInfoEndpoint()
- UserInfoEndpoint = RealmBaseUrl() & "/protocol/openid-connect/userinfo"
- End Function
-
- Public Function LogoutEndpoint()
- LogoutEndpoint = RealmBaseUrl() & "/protocol/openid-connect/logout"
- End Function
-
- Public Function BuildLoginUrl(ByVal state, ByVal nonce)
- Dim loginUrl
-
- loginUrl = AuthorizationEndpoint()
- loginUrl = loginUrl & "?client_id=" & UrlEncode(ClientId)
- loginUrl = loginUrl & "&response_type=code"
- loginUrl = loginUrl & "&scope=" & UrlEncode(Scope)
- loginUrl = loginUrl & "&redirect_uri=" & UrlEncode(RedirectUri)
-
- If Len(state) > 0 Then loginUrl = loginUrl & "&state=" & UrlEncode(state)
- If Len(nonce) > 0 Then loginUrl = loginUrl & "&nonce=" & UrlEncode(nonce)
-
- BuildLoginUrl = loginUrl
- End Function
-
- Public Sub Login()
- Dim state, nonce, loginUrl
-
- If Not IsConfigured() Then
- Err.Raise vbObjectError + 5100, "KeycloakAuth.Login", "Keycloak is not configured. Set KeycloakBaseUrl, KeycloakRealm, KeycloakClientId, and KeycloakRedirectUri."
- End If
-
- If Not ValidateOperationalConfiguration(KeycloakReadAppSetting("Environment", "Development")) Then
- Call LogDiagnostic("ERROR", "config.invalid_for_environment", ErrorMessage, "environment=" & SafeLogValue(KeycloakReadAppSetting("Environment", "Development")))
- Err.Raise vbObjectError + 5101, "KeycloakAuth.Login", ErrorMessage
- End If
-
- Call ClearPendingLoginArtifacts()
- state = CreateRandomValue()
- nonce = CreateRandomValue()
- loginUrl = BuildLoginUrl(state, nonce)
- Call WritePendingLoginCookie(SessionPrefix & "State", state)
- Call WritePendingLoginCookie(SessionPrefix & "Nonce", nonce)
- Call LogDiagnostic("INFO", "login.pending_values_issued", "Issued new Keycloak login state and nonce.", "state=" & SensitiveValueSummary(state) & " nonce=" & SensitiveValueSummary(nonce))
-
- Response.Redirect loginUrl
- End Sub
-
- Public Function HandleCallback()
- Dim callbackError, callbackDescription, code, state, expectedNonce, accessToken, userData
-
- HandleCallback = False
- ErrorMessage = ""
-
- callbackError = Trim(CStr(Request.QueryString("error")))
- If Len(callbackError) > 0 Then
- callbackDescription = Trim(CStr(Request.QueryString("error_description")))
- ErrorMessage = "Keycloak returned an error: " & callbackError
- If Len(callbackDescription) > 0 Then ErrorMessage = ErrorMessage & " - " & callbackDescription
- Exit Function
- End If
-
- code = Trim(CStr(Request.QueryString("code")))
- If Len(code) = 0 Then
- ErrorMessage = "Keycloak callback did not include an authorization code."
- Call ClearPendingLoginArtifacts()
- Exit Function
- End If
-
- state = Trim(CStr(Request.QueryString("state")))
- If Not ValidateState(state) Then
- ErrorMessage = StateValidationError(state)
- Call ClearPendingLoginArtifacts()
- Exit Function
- End If
-
- expectedNonce = ReadRequestCookie(SessionPrefix & "Nonce")
- If Len(expectedNonce) = 0 Then
- ErrorMessage = "The login session nonce is missing, so the Keycloak callback cannot be validated."
- Call ClearPendingLoginArtifacts()
- Exit Function
- End If
-
- If Not ExchangeCode(code, expectedNonce) Then
- Call ClearPendingLoginArtifacts()
- Exit Function
- End If
-
- accessToken = DictionaryString(TokenResponse, "access_token")
- If Len(accessToken) > 0 Then
- Set userData = GetUserInfo(accessToken)
- If userData Is Nothing And Len(ErrorMessage) > 0 Then
- Call LogDiagnostic("WARN", "userinfo.unavailable", ErrorMessage, "status=" & CStr(LastStatus))
- ErrorMessage = ""
- End If
- End If
-
- Call ClearPendingLoginArtifacts()
-
- HandleCallback = True
- End Function
-
- Public Function ExchangeCode(ByVal code, ByVal expectedNonce)
- Dim body, tokenData
-
- ExchangeCode = False
- ErrorMessage = ""
-
- If Not IsConfigured() Then
- ErrorMessage = "Keycloak is not configured."
- Exit Function
- End If
-
- body = ""
- body = AddFormValue(body, "grant_type", "authorization_code")
- body = AddFormValue(body, "client_id", ClientId)
- body = AddFormValue(body, "client_secret", ClientSecret)
- body = AddFormValue(body, "code", code)
- body = AddFormValue(body, "redirect_uri", RedirectUri)
-
- Set tokenData = RequestToken(body)
- If tokenData Is Nothing Then Exit Function
-
- If Not ValidateTokenResponse(tokenData, True, expectedNonce) Then Exit Function
-
- Set TokenResponse = tokenData
- Call StoreTokenResponse(tokenData)
- ExchangeCode = True
- End Function
-
- Public Function RefreshAccessToken(ByVal refreshToken)
- Dim body, tokenData
-
- RefreshAccessToken = False
- ErrorMessage = ""
-
- If Len(refreshToken) = 0 Then refreshToken = GetSessionRefreshToken()
- If Len(refreshToken) = 0 Then
- ErrorMessage = "No refresh token is available."
- Exit Function
- End If
-
- body = ""
- body = AddFormValue(body, "grant_type", "refresh_token")
- body = AddFormValue(body, "client_id", ClientId)
- body = AddFormValue(body, "client_secret", ClientSecret)
- body = AddFormValue(body, "refresh_token", refreshToken)
-
- Set tokenData = RequestToken(body)
- If tokenData Is Nothing Then Exit Function
-
- If Not ValidateTokenResponse(tokenData, False, "") Then Exit Function
-
- Set TokenResponse = tokenData
- Call StoreTokenResponse(tokenData)
- RefreshAccessToken = True
- End Function
-
- Public Function GetUserInfo(ByVal accessToken)
- Dim parsed
-
- Set GetUserInfo = Nothing
- ErrorMessage = ""
-
- If Len(accessToken) = 0 Then accessToken = GetSessionAccessToken()
- If Len(accessToken) = 0 Then
- ErrorMessage = "No access token is available."
- Exit Function
- End If
-
- If Not SendBearerGet(UserInfoEndpoint(), accessToken) Then Exit Function
- If Not IsSuccessStatus(LastStatus) Then
- ErrorMessage = "Keycloak userinfo request failed with HTTP " & LastStatus & "."
- Exit Function
- End If
-
- Set parsed = ParseJsonObject(LastResponseText)
- If parsed Is Nothing Then Exit Function
-
- Set UserInfo = parsed
- Session(SessionPrefix & "UserInfoJson") = LastResponseText
- Set GetUserInfo = parsed
- End Function
-
- Public Function CurrentUser()
- Dim userInfoJson, idToken
-
- Set CurrentUser = Nothing
-
- If IsObject(UserInfo) Then
- If Not UserInfo Is Nothing Then
- Set CurrentUser = UserInfo
- Exit Function
- End If
- End If
-
- userInfoJson = ReadSessionValue(SessionPrefix & "UserInfoJson")
- If Len(userInfoJson) > 0 Then
- Set UserInfo = ParseJsonObject(userInfoJson)
- If Not UserInfo Is Nothing Then
- Set CurrentUser = UserInfo
- Exit Function
- End If
- End If
-
- idToken = GetSessionIdToken()
- If Len(idToken) > 0 Then
- Set CurrentUser = GetTokenClaims(idToken)
- End If
- End Function
-
- Public Function GetTokenClaims(ByVal token)
- Dim parts, payloadJson
-
- Set GetTokenClaims = Nothing
- If Len(token) = 0 Then Exit Function
-
- parts = Split(token, ".")
- If UBound(parts) < 1 Then
- ErrorMessage = "The token is not a JWT."
- Exit Function
- End If
-
- payloadJson = Base64UrlDecodeToString(parts(1))
- If Len(payloadJson) = 0 Then Exit Function
-
- Set GetTokenClaims = ParseJsonObject(payloadJson)
- End Function
-
- Public Function RoleClaims()
- Dim idToken
-
- Set RoleClaims = Nothing
-
- idToken = GetSessionIdToken()
- If Len(idToken) > 0 Then
- Set RoleClaims = GetTokenClaims(idToken)
- If Not RoleClaims Is Nothing Then Exit Function
- End If
-
- Set RoleClaims = CurrentUser()
- End Function
-
- Public Function HasRealmRole(ByVal roleName)
- Dim claims, realmAccess
-
- HasRealmRole = False
- Set claims = RoleClaims()
- If claims Is Nothing Then Exit Function
-
- Set realmAccess = NestedDictionary(claims, "realm_access")
- If realmAccess Is Nothing Then Exit Function
-
- HasRealmRole = CollectionContainsText(NestedDictionary(realmAccess, "roles"), roleName)
- End Function
-
- Public Function HasClientRole(ByVal clientId, ByVal roleName)
- Dim claims, resourceAccess, clientAccess
-
- HasClientRole = False
- Set claims = RoleClaims()
- If claims Is Nothing Then Exit Function
-
- Set resourceAccess = NestedDictionary(claims, "resource_access")
- If resourceAccess Is Nothing Then Exit Function
-
- Set clientAccess = NestedDictionary(resourceAccess, clientId)
- If clientAccess Is Nothing Then Exit Function
-
- HasClientRole = CollectionContainsText(NestedDictionary(clientAccess, "roles"), roleName)
- End Function
-
- Public Function RequireLogin(ByVal returnToPath)
- RequireLogin = True
-
- If IsLoggedIn() Then Exit Function
-
- If Len(Trim(CStr(returnToPath))) = 0 Then
- returnToPath = CurrentRequestPathAndQuery()
- End If
-
- Call SetPostLoginRedirectPath(returnToPath)
- Call Login()
- RequireLogin = False
- End Function
-
- Public Sub SetPostLoginRedirectPath(ByVal returnToPath)
- Dim normalizedPath
-
- normalizedPath = NormalizeRelativeReturnPath(returnToPath)
- If Len(normalizedPath) = 0 Then
- Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
- Exit Sub
- End If
-
- If IsAuthPath(normalizedPath) Then
- Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
- Exit Sub
- End If
-
- Session(SessionPrefix & "PostLoginRedirectPath") = normalizedPath
- End Sub
-
- Public Function ConsumePostLoginRedirectPath(ByVal fallbackPath)
- Dim storedPath, normalizedFallback
-
- storedPath = NormalizeRelativeReturnPath(ReadSessionValue(SessionPrefix & "PostLoginRedirectPath"))
- Session.Contents.Remove(SessionPrefix & "PostLoginRedirectPath")
-
- If Len(storedPath) > 0 Then
- ConsumePostLoginRedirectPath = storedPath
- Exit Function
- End If
-
- normalizedFallback = NormalizeRelativeReturnPath(fallbackPath)
- If Len(normalizedFallback) > 0 Then
- ConsumePostLoginRedirectPath = normalizedFallback
- Else
- ConsumePostLoginRedirectPath = "/"
- End If
- End Function
-
- Public Function NormalizeRelativeReturnPath(ByVal returnToPath)
- Dim normalizedPath
-
- NormalizeRelativeReturnPath = ""
- normalizedPath = Trim(CStr(returnToPath))
- If Len(normalizedPath) = 0 Then Exit Function
-
- normalizedPath = Replace(normalizedPath, vbCr, "")
- normalizedPath = Replace(normalizedPath, vbLf, "")
-
- If InStr(1, normalizedPath, "://", vbBinaryCompare) > 0 Then Exit Function
- If Left(normalizedPath, 2) = "//" Then Exit Function
-
- If Left(normalizedPath, 1) = "?" Then
- normalizedPath = "/" & normalizedPath
- End If
-
- If Left(normalizedPath, 1) <> "/" Then Exit Function
-
- NormalizeRelativeReturnPath = normalizedPath
- End Function
-
- Public Function ValidateIdToken(ByVal token, ByVal expectedNonce, ByVal requireNonce)
- Dim claims
-
- ValidateIdToken = False
- ErrorMessage = ""
-
- If Len(token) = 0 Then
- ErrorMessage = "Keycloak did not return an ID token."
- Call LogDiagnostic("ERROR", "id_token.missing", ErrorMessage, "")
- Exit Function
- End If
-
- Set claims = GetTokenClaims(token)
- If claims Is Nothing Then
- Call LogDiagnostic("ERROR", "id_token.decode_failed", ErrorMessage, "")
- Exit Function
- End If
-
- ValidateIdToken = ValidateIdTokenClaims(claims, expectedNonce, requireNonce)
- End Function
-
- Public Function IsLoggedIn()
- IsLoggedIn = (Len(GetSessionAccessToken()) > 0)
- End Function
-
- Public Function GetSessionAccessToken()
- GetSessionAccessToken = ReadSessionValue(SessionPrefix & "AccessToken")
- End Function
-
- Public Function GetSessionRefreshToken()
- GetSessionRefreshToken = ReadSessionValue(SessionPrefix & "RefreshToken")
- End Function
-
- Public Function GetSessionIdToken()
- GetSessionIdToken = ReadSessionValue(SessionPrefix & "IdToken")
- End Function
-
- Public Function BuildLogoutUrl(ByVal postLogoutRedirectUri)
- Dim logoutUrl, idToken
-
- logoutUrl = LogoutEndpoint()
- If Len(logoutUrl) = 0 Then
- BuildLogoutUrl = ""
- Exit Function
- End If
-
- If Len(postLogoutRedirectUri) = 0 Then postLogoutRedirectUri = LogoutRedirectUri
-
- logoutUrl = logoutUrl & "?client_id=" & UrlEncode(ClientId)
-
- idToken = GetSessionIdToken()
- If Len(idToken) > 0 Then logoutUrl = logoutUrl & "&id_token_hint=" & UrlEncode(idToken)
- If Len(postLogoutRedirectUri) > 0 Then logoutUrl = logoutUrl & "&post_logout_redirect_uri=" & UrlEncode(postLogoutRedirectUri)
-
- BuildLogoutUrl = logoutUrl
- End Function
-
- Public Sub Logout(ByVal postLogoutRedirectUri)
- Dim logoutUrl
-
- logoutUrl = BuildLogoutUrl(postLogoutRedirectUri)
- Call ClearSession()
-
- If Len(logoutUrl) > 0 Then Response.Redirect logoutUrl
- End Sub
-
- Public Sub ClearSession()
- On Error Resume Next
- Session.Contents.Remove(SessionPrefix & "AccessToken")
- Session.Contents.Remove(SessionPrefix & "RefreshToken")
- Session.Contents.Remove(SessionPrefix & "IdToken")
- Session.Contents.Remove(SessionPrefix & "TokenType")
- Session.Contents.Remove(SessionPrefix & "ExpiresAt")
- Session.Contents.Remove(SessionPrefix & "RefreshExpiresAt")
- Session.Contents.Remove(SessionPrefix & "UserInfoJson")
- Call ClearPendingLoginArtifacts()
- Set TokenResponse = Nothing
- Set UserInfo = Nothing
- On Error GoTo 0
- End Sub
-
- Private Function RequestToken(ByVal body)
- Dim parsedToken
-
- Set RequestToken = Nothing
-
- If Not SendFormPost(TokenEndpoint(), body) Then Exit Function
- If Not IsSuccessStatus(LastStatus) Then
- ErrorMessage = "Keycloak token request failed with HTTP " & LastStatus & "."
- Call LogDiagnostic("WARN", "token.http_error", ErrorMessage, BuildHttpDetail("POST", TokenEndpoint(), LastStatus, LastResponseText))
- Exit Function
- End If
-
- Set parsedToken = ParseJsonObject(LastResponseText)
- If parsedToken Is Nothing Then
- Call LogDiagnostic("ERROR", "token.parse_failed", ErrorMessage, BuildHttpDetail("POST", TokenEndpoint(), LastStatus, LastResponseText))
- Exit Function
- End If
-
- Set RequestToken = parsedToken
- End Function
-
- Private Sub StoreTokenResponse(ByVal tokenData)
- Dim expiresIn, refreshExpiresIn
-
- If tokenData Is Nothing Then Exit Sub
-
- If tokenData.Exists("access_token") Then Session(SessionPrefix & "AccessToken") = CStr(tokenData.Item("access_token"))
- If tokenData.Exists("refresh_token") Then Session(SessionPrefix & "RefreshToken") = CStr(tokenData.Item("refresh_token"))
- If tokenData.Exists("id_token") Then Session(SessionPrefix & "IdToken") = CStr(tokenData.Item("id_token"))
- If tokenData.Exists("token_type") Then Session(SessionPrefix & "TokenType") = CStr(tokenData.Item("token_type"))
-
- If tokenData.Exists("expires_in") Then
- expiresIn = CLng(tokenData.Item("expires_in"))
- Session(SessionPrefix & "ExpiresAt") = DateAdd("s", expiresIn, Now())
- End If
-
- If tokenData.Exists("refresh_expires_in") Then
- refreshExpiresIn = CLng(tokenData.Item("refresh_expires_in"))
- Session(SessionPrefix & "RefreshExpiresAt") = DateAdd("s", refreshExpiresIn, Now())
- End If
- End Sub
-
- Private Function ValidateState(ByVal callbackState)
- ValidateState = StatesMatch(ReadRequestCookie(SessionPrefix & "State"), callbackState)
- End Function
-
- Public Function StateValidationError(ByVal callbackState)
- Dim expectedState
-
- StateValidationError = ""
- expectedState = ReadRequestCookie(SessionPrefix & "State")
-
- If Len(NormalizeStateValue(expectedState)) = 0 Then
- 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."
- Exit Function
- End If
-
- If Len(NormalizeStateValue(callbackState)) = 0 Then
- StateValidationError = "Keycloak callback did not include a state value."
- Exit Function
- End If
-
- 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."
- End Function
-
- Private Function ValidateTokenResponse(ByVal tokenData, ByVal requireNonce, ByVal expectedNonce)
- Dim accessToken, tokenType, idToken
-
- ValidateTokenResponse = False
- If tokenData Is Nothing Then Exit Function
-
- accessToken = DictionaryString(tokenData, "access_token")
- If Len(accessToken) = 0 Then
- ErrorMessage = "Keycloak token response did not include an access token."
- Call LogDiagnostic("ERROR", "token.access_token_missing", ErrorMessage, "")
- Exit Function
- End If
-
- tokenType = DictionaryString(tokenData, "token_type")
- If Len(tokenType) > 0 Then
- If StrComp(tokenType, "Bearer", vbTextCompare) <> 0 Then
- ErrorMessage = "Keycloak token response returned an unexpected token type."
- Call LogDiagnostic("ERROR", "token.invalid_type", ErrorMessage, "token_type=" & SafeLogValue(tokenType))
- Exit Function
- End If
- End If
-
- idToken = DictionaryString(tokenData, "id_token")
- If requireNonce Then
- If Len(idToken) = 0 Then
- ErrorMessage = "Keycloak token response did not include an ID token for nonce validation."
- Call LogDiagnostic("ERROR", "id_token.required_for_nonce", ErrorMessage, "")
- Exit Function
- End If
-
- End If
-
- If Len(idToken) > 0 Then
- If Not ValidateIdToken(idToken, expectedNonce, requireNonce) Then Exit Function
- End If
-
- ValidateTokenResponse = True
- End Function
-
- Private Function ValidateIdTokenClaims(ByVal claims, ByVal expectedNonce, ByVal requireNonce)
- Dim issuer, subject, nonceValue, authorizedParty
- Dim nowEpoch, expiresAt, notBefore, issuedAt, audienceCount
-
- ValidateIdTokenClaims = False
- If claims Is Nothing Then Exit Function
-
- issuer = DictionaryString(claims, "iss")
- If Len(issuer) = 0 Then
- ErrorMessage = "Keycloak ID token is missing the issuer claim."
- Call LogDiagnostic("ERROR", "id_token.iss_missing", ErrorMessage, "")
- Exit Function
- End If
-
- If StrComp(issuer, RealmBaseUrl(), vbBinaryCompare) <> 0 Then
- ErrorMessage = "Keycloak ID token issuer did not match the configured realm."
- Call LogDiagnostic("ERROR", "id_token.iss_invalid", ErrorMessage, "expected_iss=" & SafeLogValue(RealmBaseUrl()) & " actual_iss=" & SafeLogValue(issuer))
- Exit Function
- End If
-
- subject = DictionaryString(claims, "sub")
- If Len(subject) = 0 Then
- ErrorMessage = "Keycloak ID token is missing the subject claim."
- Call LogDiagnostic("ERROR", "id_token.sub_missing", ErrorMessage, "")
- Exit Function
- End If
-
- If Not AudienceContainsClientId(claims, audienceCount) Then
- ErrorMessage = "Keycloak ID token audience did not include the configured client."
- Call LogDiagnostic("ERROR", "id_token.aud_invalid", ErrorMessage, "client_id=" & SafeLogValue(ClientId) & " aud=" & SafeLogValue(ClaimValueForLog(claims, "aud")))
- Exit Function
- End If
-
- If audienceCount > 1 Then
- authorizedParty = DictionaryString(claims, "azp")
- If Len(authorizedParty) = 0 Or StrComp(authorizedParty, ClientId, vbBinaryCompare) <> 0 Then
- ErrorMessage = "Keycloak ID token authorized party did not match the configured client."
- Call LogDiagnostic("ERROR", "id_token.azp_invalid", ErrorMessage, "client_id=" & SafeLogValue(ClientId) & " azp=" & SafeLogValue(authorizedParty))
- Exit Function
- End If
- End If
-
- If Not TryGetNumericClaim(claims, "exp", expiresAt) Then
- ErrorMessage = "Keycloak ID token is missing the expiration claim."
- Call LogDiagnostic("ERROR", "id_token.exp_missing", ErrorMessage, "")
- Exit Function
- End If
-
- nowEpoch = CurrentUnixTime()
- If (expiresAt + CLng(AllowedClockSkewSeconds)) < nowEpoch Then
- ErrorMessage = "Keycloak ID token has expired."
- Call LogDiagnostic("ERROR", "id_token.expired", ErrorMessage, "exp=" & CStr(expiresAt) & " now=" & CStr(nowEpoch))
- Exit Function
- End If
-
- If TryGetNumericClaim(claims, "nbf", notBefore) Then
- If (notBefore - CLng(AllowedClockSkewSeconds)) > nowEpoch Then
- ErrorMessage = "Keycloak ID token is not valid yet."
- Call LogDiagnostic("ERROR", "id_token.not_yet_valid", ErrorMessage, "nbf=" & CStr(notBefore) & " now=" & CStr(nowEpoch))
- Exit Function
- End If
- End If
-
- If TryGetNumericClaim(claims, "iat", issuedAt) Then
- If (issuedAt - CLng(AllowedClockSkewSeconds)) > nowEpoch Then
- ErrorMessage = "Keycloak ID token appears to have been issued in the future."
- Call LogDiagnostic("ERROR", "id_token.issued_at_invalid", ErrorMessage, "iat=" & CStr(issuedAt) & " now=" & CStr(nowEpoch))
- Exit Function
- End If
- End If
-
- If requireNonce Then
- If Len(expectedNonce) = 0 Then
- ErrorMessage = "The login session nonce is missing, so the Keycloak callback cannot be validated."
- Call LogDiagnostic("ERROR", "id_token.nonce_session_missing", ErrorMessage, "")
- Exit Function
- End If
-
- nonceValue = DictionaryString(claims, "nonce")
- If Len(nonceValue) = 0 Then
- ErrorMessage = "Keycloak ID token is missing the nonce claim."
- Call LogDiagnostic("ERROR", "id_token.nonce_missing", ErrorMessage, "")
- Exit Function
- End If
-
- If StrComp(nonceValue, expectedNonce, vbBinaryCompare) <> 0 Then
- ErrorMessage = "Keycloak ID token nonce did not match the active login session."
- Call LogDiagnostic("ERROR", "id_token.nonce_invalid", ErrorMessage, "expected_nonce=" & SensitiveValueSummary(expectedNonce) & " actual_nonce=" & SensitiveValueSummary(nonceValue))
- Exit Function
- End If
- End If
-
- ValidateIdTokenClaims = True
- End Function
-
- Private Function SendFormPost(ByVal url, ByVal body)
- Dim http
-
- SendFormPost = False
- LastStatus = 0
- LastResponseText = ""
-
- Set http = CreateHttpClient()
- If http Is Nothing Then Exit Function
-
- On Error Resume Next
- http.open "POST", url, False
- http.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
- http.setRequestHeader "Accept", "application/json"
- http.send body
-
- If Err.Number <> 0 Then
- ErrorMessage = "Keycloak HTTP POST failed: " & Err.Description
- Call LogDiagnostic("ERROR", "http.post_failed", ErrorMessage, BuildHttpDetail("POST", url, 0, ""))
- Err.Clear
- Else
- LastStatus = CLng(http.status)
- LastResponseText = CStr(http.responseText)
- SendFormPost = True
- End If
-
- Set http = Nothing
- On Error GoTo 0
- End Function
-
- Private Function SendBearerGet(ByVal url, ByVal bearerToken)
- Dim http
-
- SendBearerGet = False
- LastStatus = 0
- LastResponseText = ""
-
- Set http = CreateHttpClient()
- If http Is Nothing Then Exit Function
-
- On Error Resume Next
- http.open "GET", url, False
- http.setRequestHeader "Accept", "application/json"
- http.setRequestHeader "Authorization", "Bearer " & bearerToken
- http.send
-
- If Err.Number <> 0 Then
- ErrorMessage = "Keycloak HTTP GET failed: " & Err.Description
- Call LogDiagnostic("ERROR", "http.get_failed", ErrorMessage, BuildHttpDetail("GET", url, 0, ""))
- Err.Clear
- Else
- LastStatus = CLng(http.status)
- LastResponseText = CStr(http.responseText)
- SendBearerGet = True
- End If
-
- Set http = Nothing
- On Error GoTo 0
- End Function
-
- Private Function ParseJsonObject(ByVal jsonText)
- Dim parser
-
- Set ParseJsonObject = Nothing
- If Len(Trim(CStr(jsonText))) = 0 Then Exit Function
-
- On Error Resume Next
- Set parser = New aspJSON
- parser.loadJSON jsonText
-
- If Err.Number <> 0 Then
- ErrorMessage = "Unable to parse Keycloak JSON response: " & Err.Description
- Call LogDiagnostic("ERROR", "json.parse_failed", ErrorMessage, "json_length=" & CStr(Len(CStr(jsonText))))
- Err.Clear
- Else
- Set ParseJsonObject = parser.data
- End If
-
- On Error GoTo 0
- End Function
-
- Private Function Base64UrlDecodeToString(ByVal value)
- Dim base64Value, xml, node, bytes, stream
-
- Base64UrlDecodeToString = ""
- base64Value = Replace(Replace(CStr(value), "-", "+"), "_", "/")
-
- Do While (Len(base64Value) Mod 4) <> 0
- base64Value = base64Value & "="
- Loop
-
- On Error Resume Next
- Set xml = Server.CreateObject("MSXML2.DOMDocument.6.0")
- If Err.Number <> 0 Then
- Err.Clear
- Set xml = Server.CreateObject("MSXML2.DOMDocument")
- End If
-
- Set node = xml.createElement("base64")
- node.DataType = "bin.base64"
- node.Text = base64Value
- bytes = node.nodeTypedValue
-
- Set stream = Server.CreateObject("ADODB.Stream")
- stream.Type = 1
- stream.Open
- stream.Write bytes
- stream.Position = 0
- stream.Type = 2
- stream.Charset = "utf-8"
- Base64UrlDecodeToString = stream.ReadText
- stream.Close
-
- If Err.Number <> 0 Then
- ErrorMessage = "Unable to decode JWT payload: " & Err.Description
- Base64UrlDecodeToString = ""
- Call LogDiagnostic("ERROR", "jwt.decode_failed", ErrorMessage, "")
- Err.Clear
- End If
-
- Set stream = Nothing
- Set node = Nothing
- Set xml = Nothing
- On Error GoTo 0
- End Function
-
- Private Function NormalizeBaseUrl(ByVal value)
- value = Trim(CStr(value))
-
- Do While Right(value, 1) = "/"
- value = Left(value, Len(value) - 1)
- Loop
-
- NormalizeBaseUrl = value
- End Function
-
- Private Function CurrentRequestPathAndQuery()
- Dim requestPath, queryString
-
- CurrentRequestPathAndQuery = ""
-
- On Error Resume Next
- requestPath = Trim(CStr(Request.ServerVariables("HTTP_X_ORIGINAL_URL")))
- If Len(requestPath) = 0 Then requestPath = Trim(CStr(Request.ServerVariables("URL")))
- queryString = Trim(CStr(Request.ServerVariables("QUERY_STRING")))
- On Error GoTo 0
-
- If Len(requestPath) = 0 Then requestPath = "/"
- If Len(queryString) > 0 And InStr(1, requestPath, "?", vbBinaryCompare) = 0 Then
- requestPath = requestPath & "?" & queryString
- End If
-
- CurrentRequestPathAndQuery = requestPath
- End Function
-
- Private Function IsHttpsUrl(ByVal value)
- IsHttpsUrl = (LCase(Left(Trim(CStr(value)), 8)) = "https://")
- End Function
-
- Private Function UrlTargetsLocalhost(ByVal value)
- Dim normalizedValue
-
- normalizedValue = LCase(Trim(CStr(value)))
- UrlTargetsLocalhost = (InStr(1, normalizedValue, "://localhost", vbBinaryCompare) > 0 Or _
- InStr(1, normalizedValue, "://127.0.0.1", vbBinaryCompare) > 0 Or _
- InStr(1, normalizedValue, "://[::1]", vbBinaryCompare) > 0)
- End Function
-
- Private Function NestedDictionary(ByVal parentDictionary, ByVal key)
- Set NestedDictionary = Nothing
-
- If Not IsObject(parentDictionary) Then Exit Function
- If parentDictionary Is Nothing Then Exit Function
- If Not parentDictionary.Exists(key) Then Exit Function
- If Not IsObject(parentDictionary.Item(key)) Then Exit Function
-
- Set NestedDictionary = parentDictionary.Item(key)
- End Function
-
- Private Function CollectionContainsText(ByVal dictionary, ByVal expectedValue)
- Dim itemKey, itemValue
-
- CollectionContainsText = False
- If Len(Trim(CStr(expectedValue))) = 0 Then Exit Function
- If Not IsObject(dictionary) Then Exit Function
- If dictionary Is Nothing Then Exit Function
-
- For Each itemKey In dictionary.Keys
- itemValue = dictionary.Item(itemKey)
- If Not IsNull(itemValue) And Not IsEmpty(itemValue) Then
- If StrComp(CStr(itemValue), CStr(expectedValue), vbTextCompare) = 0 Then
- CollectionContainsText = True
- Exit Function
- End If
- End If
- Next
- End Function
-
- Private Function IsAuthPath(ByVal path)
- Dim normalizedPath
-
- normalizedPath = LCase(CStr(path))
- IsAuthPath = (Left(normalizedPath, 11) = "/auth/login" Or _
- Left(normalizedPath, 14) = "/auth/callback" Or _
- Left(normalizedPath, 12) = "/auth/logout")
- End Function
-
- Private Function AddFormValue(ByVal body, ByVal key, ByVal value)
- If Len(CStr(value)) = 0 Then
- AddFormValue = body
- Exit Function
- End If
-
- If Len(body) > 0 Then body = body & "&"
- AddFormValue = body & UrlEncode(key) & "=" & UrlEncode(value)
- End Function
-
- Private Function UrlEncode(ByVal value)
- UrlEncode = Server.URLEncode(CStr(value))
- End Function
-
- Private Function DictionaryString(ByVal dictionary, ByVal key)
- DictionaryString = ""
-
- If Not IsObject(dictionary) Then Exit Function
- If dictionary Is Nothing Then Exit Function
- If Not dictionary.Exists(key) Then Exit Function
- If IsNull(dictionary.Item(key)) Or IsEmpty(dictionary.Item(key)) Then Exit Function
-
- DictionaryString = CStr(dictionary.Item(key))
- End Function
-
- Private Function ReadSessionValue(ByVal key)
- Dim value
-
- ReadSessionValue = ""
-
- On Error Resume Next
- value = Session(key)
- If Err.Number <> 0 Then
- Err.Clear
- On Error GoTo 0
- Exit Function
- End If
- On Error GoTo 0
-
- If IsNull(value) Or IsEmpty(value) Then Exit Function
- ReadSessionValue = CStr(value)
- End Function
-
- Public Function EnsurePendingLoginValue(ByVal key)
- Dim value
-
- value = ReadPendingLoginValue(key)
- If Len(value) = 0 Then
- value = CreateRandomValue()
- Session(key) = value
- Call WritePendingLoginCookie(key, value)
- End If
-
- EnsurePendingLoginValue = value
- End Function
-
- Private Function ReadPendingLoginValue(ByVal key)
- ReadPendingLoginValue = ReadSessionValue(key)
- If Len(ReadPendingLoginValue) > 0 Then Exit Function
-
- ReadPendingLoginValue = ReadRequestCookie(key)
- End Function
-
- Private Sub ClearPendingLoginArtifacts()
- On Error Resume Next
- Session.Contents.Remove(SessionPrefix & "State")
- Session.Contents.Remove(SessionPrefix & "Nonce")
- Session.Contents.Remove(SessionPrefix & "LastIssuedState")
- Call ClearPendingLoginCookie(SessionPrefix & "PendingLogin")
- Call ClearPendingLoginCookie(SessionPrefix & "State")
- Call ClearPendingLoginCookie(SessionPrefix & "Nonce")
- Call ClearPendingLoginCookie(SessionPrefix & "LastIssuedState")
- Err.Clear
- On Error GoTo 0
- End Sub
-
- Private Function ReadRequestCookie(ByVal key)
- Dim cookieHeader, cookieParts, cookiePart, separatorPosition, currentName, currentValue
-
- ReadRequestCookie = ""
- If Len(Trim(CStr(key))) = 0 Then Exit Function
-
- On Error Resume Next
- cookieHeader = CStr(Request.ServerVariables("HTTP_COOKIE"))
- If Err.Number <> 0 Then
- Err.Clear
- On Error GoTo 0
- Exit Function
- End If
- On Error GoTo 0
-
- If Len(cookieHeader) = 0 Then Exit Function
-
- cookieParts = Split(cookieHeader, ";")
- For Each cookiePart In cookieParts
- separatorPosition = InStr(1, cookiePart, "=", vbBinaryCompare)
- If separatorPosition > 0 Then
- currentName = Trim(Left(cookiePart, separatorPosition - 1))
- currentName = DecodePercentEscapes(currentName)
- If StrComp(currentName, CStr(key), vbBinaryCompare) = 0 Then
- currentValue = Mid(cookiePart, separatorPosition + 1)
- ReadRequestCookie = Trim(DecodePercentEscapes(CStr(currentValue)))
- Exit Function
- End If
- End If
- Next
- End Function
-
- Private Function DecodePercentEscapes(ByVal value)
- Dim outputValue, position, currentChar, hexValue
-
- outputValue = ""
- position = 1
-
- Do While position <= Len(CStr(value))
- currentChar = Mid(CStr(value), position, 1)
-
- If currentChar = "%" And (position + 2) <= Len(CStr(value)) Then
- hexValue = Mid(CStr(value), position + 1, 2)
- If IsHexByte(hexValue) Then
- outputValue = outputValue & Chr(CLng("&H" & hexValue))
- position = position + 3
- Else
- outputValue = outputValue & currentChar
- position = position + 1
- End If
- Else
- outputValue = outputValue & currentChar
- position = position + 1
- End If
- Loop
-
- DecodePercentEscapes = outputValue
- End Function
-
- Private Function IsHexByte(ByVal value)
- Dim i, currentChar
-
- IsHexByte = False
- If Len(CStr(value)) <> 2 Then Exit Function
-
- For i = 1 To 2
- currentChar = UCase(Mid(CStr(value), i, 1))
- If InStr(1, "0123456789ABCDEF", currentChar, vbBinaryCompare) = 0 Then Exit Function
- Next
-
- IsHexByte = True
- End Function
-
- Private Sub WritePendingLoginCookie(ByVal key, ByVal value)
- If Len(Trim(CStr(key))) = 0 Then Exit Sub
-
- On Error Resume Next
- Response.Cookies(key) = CStr(value)
- Response.Cookies(key).Path = "/"
- Response.Cookies(key).Expires = DateAdd("n", CLng(PendingLoginCookieMinutes), Now())
- Response.Cookies(key).HttpOnly = True
- Err.Clear
- On Error GoTo 0
- End Sub
-
- Private Sub ClearPendingLoginCookie(ByVal key)
- If Len(Trim(CStr(key))) = 0 Then Exit Sub
-
- On Error Resume Next
- Response.Cookies(key) = ""
- Response.Cookies(key).Path = "/"
- Response.Cookies(key).Expires = DateAdd("d", -1, Now())
- Response.Cookies(key).HttpOnly = True
- Err.Clear
- On Error GoTo 0
- End Sub
-
- Private Function NormalizeStateValue(ByVal value)
- Dim textValue, i, currentChar, normalizedValue
-
- textValue = UCase(Trim(CStr(value)))
- normalizedValue = ""
-
- For i = 1 To Len(textValue)
- currentChar = Mid(textValue, i, 1)
- If (currentChar >= "A" And currentChar <= "Z") Or (currentChar >= "0" And currentChar <= "9") Then
- normalizedValue = normalizedValue & currentChar
- End If
- Next
-
- NormalizeStateValue = normalizedValue
- End Function
-
- Private Function StatesMatch(ByVal leftValue, ByVal rightValue)
- Dim normalizedLeftValue, normalizedRightValue
-
- normalizedLeftValue = NormalizeStateValue(leftValue)
- normalizedRightValue = NormalizeStateValue(rightValue)
-
- StatesMatch = (Len(normalizedLeftValue) > 0 And Len(normalizedRightValue) > 0 And normalizedLeftValue = normalizedRightValue)
- End Function
-
- Private Function IsSuccessStatus(ByVal statusCode)
- IsSuccessStatus = (CLng(statusCode) >= 200 And CLng(statusCode) < 300)
- End Function
-
- Private Function CreateHttpClient()
- Dim http
-
- Set CreateHttpClient = Nothing
- On Error Resume Next
-
- Set http = Server.CreateObject("MSXML2.ServerXMLHTTP.6.0")
- If Err.Number <> 0 Then
- Err.Clear
- Set http = Server.CreateObject("MSXML2.ServerXMLHTTP")
- End If
-
- If Err.Number <> 0 Then
- ErrorMessage = "Unable to create the Keycloak HTTP client: " & Err.Description
- Call LogDiagnostic("ERROR", "http.client_create_failed", ErrorMessage, "")
- Err.Clear
- Set http = Nothing
- Else
- Call ApplyHttpTimeouts(http)
- End If
-
- Set CreateHttpClient = http
- On Error GoTo 0
- End Function
-
- Private Sub ApplyHttpTimeouts(ByVal http)
- If Not IsObject(http) Then Exit Sub
- If http Is Nothing Then Exit Sub
-
- On Error Resume Next
- http.setTimeouts CLng(HttpResolveTimeoutMs), CLng(HttpConnectTimeoutMs), CLng(HttpSendTimeoutMs), CLng(HttpReceiveTimeoutMs)
- If Err.Number <> 0 Then
- 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))
- Err.Clear
- End If
- On Error GoTo 0
- End Sub
-
- Private Function AudienceContainsClientId(ByVal claims, audienceCount)
- Dim audienceValue, audienceCollection, audienceKey
-
- audienceCount = 0
- AudienceContainsClientId = False
- If Not IsObject(claims) Then Exit Function
- If claims Is Nothing Then Exit Function
- If Not claims.Exists("aud") Then Exit Function
-
- If IsObject(claims.Item("aud")) Then
- Set audienceCollection = claims.Item("aud")
- For Each audienceKey In audienceCollection.Keys
- audienceCount = audienceCount + 1
- If StrComp(CStr(audienceCollection.Item(audienceKey)), ClientId, vbBinaryCompare) = 0 Then
- AudienceContainsClientId = True
- End If
- Next
- Set audienceCollection = Nothing
- Exit Function
- End If
-
- audienceValue = claims.Item("aud")
- If IsNull(audienceValue) Or IsEmpty(audienceValue) Then Exit Function
- audienceCount = 1
- AudienceContainsClientId = (StrComp(CStr(audienceValue), ClientId, vbBinaryCompare) = 0)
- End Function
-
- Private Function TryGetNumericClaim(ByVal claims, ByVal key, numericValue)
- Dim value
-
- TryGetNumericClaim = False
- numericValue = 0
-
- If Not IsObject(claims) Then Exit Function
- If claims Is Nothing Then Exit Function
- If Not claims.Exists(key) Then Exit Function
-
- value = claims.Item(key)
- If IsNull(value) Or IsEmpty(value) Then Exit Function
- If Not IsNumeric(value) Then Exit Function
-
- numericValue = CLng(value)
- TryGetNumericClaim = True
- End Function
-
- Private Function ClaimValueForLog(ByVal claims, ByVal key)
- Dim value, claimCollection, claimKey, combinedValue
-
- ClaimValueForLog = ""
- If Not IsObject(claims) Then Exit Function
- If claims Is Nothing Then Exit Function
- If Not claims.Exists(key) Then Exit Function
-
- If IsObject(claims.Item(key)) Then
- Set claimCollection = claims.Item(key)
- combinedValue = ""
- For Each claimKey In claimCollection.Keys
- If Len(combinedValue) > 0 Then combinedValue = combinedValue & ","
- combinedValue = combinedValue & CStr(claimCollection.Item(claimKey))
- Next
- ClaimValueForLog = combinedValue
- Set claimCollection = Nothing
- Exit Function
- End If
-
- value = claims.Item(key)
- If IsNull(value) Or IsEmpty(value) Then Exit Function
- ClaimValueForLog = CStr(value)
- End Function
-
- Private Function CurrentUnixTime()
- CurrentUnixTime = DateDiff("s", CDate("01/01/1970 00:00:00"), UtcNowValue())
- End Function
-
- Private Function UtcNowValue()
- Dim biasMinutes, service, utcTimes, utcItem
-
- UtcNowValue = Now()
-
- If TryGetActiveUtcBiasMinutes(biasMinutes) Then
- UtcNowValue = DateAdd("n", CLng(biasMinutes), Now())
- Exit Function
- End If
-
- On Error Resume Next
- Set service = GetObject("winmgmts:root\cimv2")
- Set utcTimes = service.ExecQuery("SELECT * FROM Win32_UTCTime")
- For Each utcItem In utcTimes
- UtcNowValue = DateSerial(utcItem.Year, utcItem.Month, utcItem.Day) + TimeSerial(utcItem.Hour, utcItem.Minute, utcItem.Second)
- Exit For
- Next
- Err.Clear
- Set utcItem = Nothing
- Set utcTimes = Nothing
- Set service = Nothing
- On Error GoTo 0
- End Function
-
- Private Function TryGetActiveUtcBiasMinutes(ByRef biasMinutes)
- Dim shell, value
-
- TryGetActiveUtcBiasMinutes = False
- biasMinutes = 0
-
- On Error Resume Next
- Set shell = Server.CreateObject("WScript.Shell")
- value = shell.RegRead("HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias")
-
- If Err.Number = 0 Then
- biasMinutes = CLng(value)
- TryGetActiveUtcBiasMinutes = True
- Else
- Err.Clear
- End If
-
- Set shell = Nothing
- On Error GoTo 0
- End Function
-
- Private Function BuildHttpDetail(ByVal method, ByVal url, ByVal statusCode, ByVal responseText)
- 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)
- If Len(responseText) > 0 Then
- BuildHttpDetail = BuildHttpDetail & " response_length=" & CStr(Len(CStr(responseText)))
- End If
- End Function
-
- Private Sub LogDiagnostic(ByVal level, ByVal eventName, ByVal message, ByVal detail)
- Dim logEntry, appendValue
-
- logEntry = "[" & Replace(CStr(UtcNowValue()), vbCrLf, " ") & "] [Keycloak] level=" & UCase(CStr(level)) & " event=" & SafeLogValue(eventName) & " client_id=" & SafeLogValue(ClientId) & " realm=" & SafeLogValue(Realm) & " message=" & SafeLogValue(message)
- If Len(detail) > 0 Then logEntry = logEntry & " detail=" & SafeLogValue(detail)
-
- appendValue = "kc " & Left(CStr(eventName) & " " & TruncateForLog(message, 48), 80)
-
- On Error Resume Next
- Response.AppendToLog appendValue
- If EnableDiagnosticLogging And Len(DiagnosticLogPath) > 0 Then
- Call AppendLogFile(logEntry)
- End If
- Err.Clear
- On Error GoTo 0
- End Sub
-
- Private Sub AppendLogFile(ByVal logEntry)
- Dim fso, logFile
-
- If Len(DiagnosticLogPath) = 0 Then Exit Sub
-
- On Error Resume Next
- Set fso = Server.CreateObject("Scripting.FileSystemObject")
- If fso.FileExists(DiagnosticLogPath) Then
- Set logFile = fso.OpenTextFile(DiagnosticLogPath, 8, False)
- Else
- Set logFile = fso.CreateTextFile(DiagnosticLogPath, True)
- End If
-
- logFile.WriteLine logEntry
- logFile.Close
-
- Set logFile = Nothing
- Set fso = Nothing
- Err.Clear
- On Error GoTo 0
- End Sub
-
- Private Function TruncateForLog(ByVal value, ByVal maxLength)
- Dim textValue
-
- textValue = CStr(value)
- If Len(textValue) <= CLng(maxLength) Then
- TruncateForLog = textValue
- Else
- TruncateForLog = Left(textValue, CLng(maxLength) - 3) & "..."
- End If
- End Function
-
- Private Function SafeLogValue(ByVal value)
- Dim textValue
-
- textValue = Replace(CStr(value), vbCr, " ")
- textValue = Replace(textValue, vbLf, " ")
- textValue = Replace(textValue, "|", "/")
- SafeLogValue = Trim(textValue)
- End Function
-
- Private Function SensitiveValueSummary(ByVal value)
- SensitiveValueSummary = "len=" & CStr(Len(CStr(value)))
- End Function
-
- Private Function CreateRandomValue()
- Dim guidProvider, rawValue
-
- Randomize
- rawValue = ""
-
- On Error Resume Next
- Set guidProvider = Server.CreateObject("Scriptlet.TypeLib")
- If Err.Number = 0 Then rawValue = guidProvider.Guid
- If Err.Number <> 0 Or Len(rawValue) = 0 Then
- Err.Clear
- rawValue = CStr(Now()) & "-" & CStr(Timer) & "-" & CStr(Rnd())
- End If
- On Error GoTo 0
-
- rawValue = Replace(rawValue, "{", "")
- rawValue = Replace(rawValue, "}", "")
- rawValue = Replace(rawValue, "-", "")
- rawValue = Replace(rawValue, " ", "")
- rawValue = Replace(rawValue, ":", "")
- rawValue = Replace(rawValue, "/", "")
- rawValue = Replace(rawValue, ".", "")
-
- CreateRandomValue = rawValue
- End Function
- End Class
-
- Function KeycloakReadAppSetting(ByVal key, ByVal fallbackValue)
- Dim value
-
- KeycloakReadAppSetting = fallbackValue
-
- On Error Resume Next
- value = GetAppSetting(key)
- If Err.Number <> 0 Then
- Err.Clear
- On Error GoTo 0
- Exit Function
- End If
- On Error GoTo 0
-
- If IsNull(value) Or IsEmpty(value) Then Exit Function
- If Len(CStr(value)) = 0 Then Exit Function
- If LCase(CStr(value)) = "nothing" Then Exit Function
-
- KeycloakReadAppSetting = CStr(value)
- End Function
-
- Function KeycloakReadAppSettingBool(ByVal key, ByVal fallbackValue)
- Dim value
-
- KeycloakReadAppSettingBool = CBool(fallbackValue)
- value = LCase(Trim(CStr(KeycloakReadAppSetting(key, ""))))
-
- If Len(value) = 0 Then Exit Function
-
- Select Case value
- Case "true", "1", "yes", "on"
- KeycloakReadAppSettingBool = True
- Case "false", "0", "no", "off"
- KeycloakReadAppSettingBool = False
- End Select
- End Function
-
- Function KeycloakReadAppSettingLong(ByVal key, ByVal fallbackValue)
- Dim value
-
- KeycloakReadAppSettingLong = CLng(fallbackValue)
- value = KeycloakReadAppSetting(key, "")
-
- If Len(Trim(CStr(value))) = 0 Then Exit Function
- If Not IsNumeric(value) Then Exit Function
-
- KeycloakReadAppSettingLong = CLng(value)
- End Function
-
- Dim KeycloakAuth_Class__Singleton
- Function KeycloakAuth()
- If IsEmpty(KeycloakAuth_Class__Singleton) Then
- Set KeycloakAuth_Class__Singleton = New KeycloakAuth_Class
- Call KeycloakAuth_Class__Singleton.ConfigureFromAppSettings()
- End If
-
- Set KeycloakAuth = KeycloakAuth_Class__Singleton
- End Function
-
- Function Keycloak()
- Set Keycloak = KeycloakAuth()
- End Function
-
- Sub KeycloakLogin()
- Dim auth
- Set auth = KeycloakAuth()
- Call auth.Login()
- End Sub
-
- Function KeycloakHandleCallback()
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakHandleCallback = auth.HandleCallback()
- End Function
-
- Function KeycloakIsLoggedIn()
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakIsLoggedIn = auth.IsLoggedIn()
- End Function
-
- Function KeycloakAccessToken()
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakAccessToken = auth.GetSessionAccessToken()
- End Function
-
- Function KeycloakRefreshToken()
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakRefreshToken = auth.GetSessionRefreshToken()
- End Function
-
- Function KeycloakIdToken()
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakIdToken = auth.GetSessionIdToken()
- End Function
-
- Function KeycloakCurrentUser()
- Dim auth
- Set auth = KeycloakAuth()
- Set KeycloakCurrentUser = auth.CurrentUser()
- End Function
-
- Function KeycloakUserInfo()
- Dim auth
- Set auth = KeycloakAuth()
- Set KeycloakUserInfo = auth.GetUserInfo("")
- End Function
-
- Function KeycloakTokenClaims(ByVal token)
- Dim auth
- Set auth = KeycloakAuth()
- Set KeycloakTokenClaims = auth.GetTokenClaims(token)
- End Function
-
- Function KeycloakRequireLogin(ByVal returnToPath)
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakRequireLogin = auth.RequireLogin(returnToPath)
- End Function
-
- Sub KeycloakSetPostLoginRedirectPath(ByVal returnToPath)
- Dim auth
- Set auth = KeycloakAuth()
- Call auth.SetPostLoginRedirectPath(returnToPath)
- End Sub
-
- Function KeycloakConsumePostLoginRedirectPath(ByVal fallbackPath)
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakConsumePostLoginRedirectPath = auth.ConsumePostLoginRedirectPath(fallbackPath)
- End Function
-
- Function KeycloakHasRealmRole(ByVal roleName)
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakHasRealmRole = auth.HasRealmRole(roleName)
- End Function
-
- Function KeycloakHasClientRole(ByVal clientId, ByVal roleName)
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakHasClientRole = auth.HasClientRole(clientId, roleName)
- End Function
-
- Function KeycloakLogoutUrl(ByVal postLogoutRedirectUri)
- Dim auth
- Set auth = KeycloakAuth()
- KeycloakLogoutUrl = auth.BuildLogoutUrl(postLogoutRedirectUri)
- End Function
-
- Sub KeycloakLogout(ByVal postLogoutRedirectUri)
- Dim auth
- Set auth = KeycloakAuth()
- Call auth.Logout(postLogoutRedirectUri)
- End Sub
- %>
|