Import Excel DY
This commit is contained in:
@@ -1,245 +1,199 @@
|
||||
Imports Newtonsoft.Json
|
||||
Imports System
|
||||
Imports System.IO
|
||||
Imports System.Net.Http
|
||||
Imports System.Text
|
||||
Imports System.Web
|
||||
Imports Newtonsoft.Json
|
||||
|
||||
' NuGet: Newtonsoft.Json (>= 13.x)
|
||||
|
||||
Public Class cRelayHubToken
|
||||
|
||||
' === Token-Datenmodell ===
|
||||
Private Class TokenState
|
||||
Public AccessToken As String
|
||||
Public RefreshToken As String
|
||||
Public AccessExpiryUtc As DateTime
|
||||
Public RefreshExpiryUtc As DateTime
|
||||
End Class
|
||||
' ======= KONFIG =======
|
||||
Private Shared ReadOnly TOKEN_ENDPOINT As String =
|
||||
"https://dev-kc.singlewindow.io/auth/realms/agsw/protocol/openid-connect/token"
|
||||
|
||||
' === Keycloak-Config ===
|
||||
Private Shared ReadOnly KC_BASE As String = "https://dev-kc.singlewindow.io"
|
||||
Private Shared ReadOnly KC_TOKEN_PATH As String = "/auth/realms/agsw/protocol/openid-connect/token"
|
||||
Private Shared ReadOnly KC_CLIENT_ID As String = "agsw-admin"
|
||||
Private Shared ReadOnly KC_USERNAME As String = "andreas.test@test.com"
|
||||
Private Shared ReadOnly KC_PASSWORD As String = "Password.123"
|
||||
Private Shared ReadOnly SKEW As TimeSpan = TimeSpan.FromSeconds(30)
|
||||
Private Shared ReadOnly CLIENT_ID As String = "agsw-admin"
|
||||
|
||||
' === Cache/Persistenz ===
|
||||
Private Shared _ts As TokenState = Nothing
|
||||
Private Shared ReadOnly TOKEN_FILE As String = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"RelayHub", "token.cache"
|
||||
)
|
||||
Private Shared ReadOnly _lockObj As New Object()
|
||||
' Gewünscht: Zugangsdaten in der Klasse definieren
|
||||
Private Shared ReadOnly USERNAME As String = "andreas.test@test.com"
|
||||
Private Shared ReadOnly PASSWORD As String = "Password.123"
|
||||
|
||||
' -------------- DPAPI via Reflection (keine Compile-Abhängigkeit!) --------------
|
||||
Private Shared Function TryProtect(plain As Byte()) As Byte()
|
||||
Try
|
||||
' Versuche: Typen aus Assembly "System.Security" oder aus aktuellen Laufzeit-Assemblys laden
|
||||
Dim dpType As Type = Type.GetType("System.Security.Cryptography.ProtectedData, System.Security", throwOnError:=False)
|
||||
If dpType Is Nothing Then
|
||||
dpType = Type.GetType("System.Security.Cryptography.ProtectedData", throwOnError:=False)
|
||||
' Token-File pro Benutzer unter %AppData%
|
||||
Private Shared ReadOnly StorePath As String =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"RelayHub", "token.json")
|
||||
|
||||
' Sicherheitspuffer, bevor wir erneuern (Sekunden)
|
||||
Private Const ExpirySkewSeconds As Integer = 60
|
||||
|
||||
' ======= ÖFFENTLICHE API =======
|
||||
''' <summary>
|
||||
''' Liefert einen gültigen Access Token (nie Leerstring).
|
||||
''' </summary>
|
||||
Public Shared Function GetAccessToken() As String
|
||||
Dim store = LoadStore()
|
||||
|
||||
' 1) Wenn wir einen (noch) gültigen Token haben
|
||||
If store IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(store.AccessToken) Then
|
||||
If store.ExpiresAtUtc > DateTimeOffset.UtcNow.AddSeconds(ExpirySkewSeconds) Then
|
||||
Return store.AccessToken
|
||||
End If
|
||||
Dim scopeType As Type = Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security", throwOnError:=False)
|
||||
If dpType Is Nothing OrElse scopeType Is Nothing Then Return Nothing
|
||||
|
||||
Dim scopeObj As Object = [Enum].Parse(scopeType, "CurrentUser")
|
||||
Dim mi = dpType.GetMethod("Protect", New Type() {GetType(Byte()), GetType(Byte()), scopeType})
|
||||
If mi Is Nothing Then Return Nothing
|
||||
|
||||
Dim res = mi.Invoke(Nothing, New Object() {plain, Nothing, scopeObj})
|
||||
Return TryCast(res, Byte())
|
||||
Catch
|
||||
Return Nothing
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Private Shared Function TryUnprotect(protectedBytes As Byte()) As Byte()
|
||||
Try
|
||||
Dim dpType As Type = Type.GetType("System.Security.Cryptography.ProtectedData, System.Security", throwOnError:=False)
|
||||
If dpType Is Nothing Then
|
||||
dpType = Type.GetType("System.Security.Cryptography.ProtectedData", throwOnError:=False)
|
||||
End If
|
||||
Dim scopeType As Type = Type.GetType("System.Security.Cryptography.DataProtectionScope, System.Security", throwOnError:=False)
|
||||
If dpType Is Nothing OrElse scopeType Is Nothing Then Return Nothing
|
||||
|
||||
Dim scopeObj As Object = [Enum].Parse(scopeType, "CurrentUser")
|
||||
Dim mi = dpType.GetMethod("Unprotect", New Type() {GetType(Byte()), GetType(Byte()), scopeType})
|
||||
If mi Is Nothing Then Return Nothing
|
||||
|
||||
Dim res = mi.Invoke(Nothing, New Object() {protectedBytes, Nothing, scopeObj})
|
||||
Return TryCast(res, Byte())
|
||||
Catch
|
||||
Return Nothing
|
||||
End Try
|
||||
End Function
|
||||
|
||||
' -------------- Persistenz: bevorzugt DPAPI, Fallback Plain-File --------------
|
||||
Private Shared Sub SaveTokenSecure(ts As TokenState)
|
||||
Try
|
||||
Dim dir = Path.GetDirectoryName(TOKEN_FILE)
|
||||
If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
|
||||
|
||||
Dim payload As String = String.Join(vbLf, {
|
||||
ts.AccessToken,
|
||||
ts.RefreshToken,
|
||||
ts.AccessExpiryUtc.Ticks.ToString(),
|
||||
ts.RefreshExpiryUtc.Ticks.ToString()
|
||||
})
|
||||
Dim plain = Encoding.UTF8.GetBytes(payload)
|
||||
|
||||
Dim protectedBytes = TryProtect(plain)
|
||||
If protectedBytes IsNot Nothing Then
|
||||
File.WriteAllBytes(TOKEN_FILE, protectedBytes)
|
||||
Else
|
||||
' Fallback (nur zu Testzwecken!)
|
||||
File.WriteAllText(TOKEN_FILE, payload, Encoding.UTF8)
|
||||
End If
|
||||
Catch
|
||||
' optional loggen
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
Private Shared Function LoadTokenSecure() As TokenState
|
||||
Try
|
||||
If Not File.Exists(TOKEN_FILE) Then Return Nothing
|
||||
|
||||
' Zuerst versuchen wir, als DPAPI-Bytes zu lesen und zu entschlüsseln
|
||||
Dim raw = File.ReadAllBytes(TOKEN_FILE)
|
||||
Dim plain = TryUnprotect(raw)
|
||||
|
||||
Dim content As String
|
||||
If plain Is Nothing Then
|
||||
' Fallback: als Text lesen (falls zuvor ohne DPAPI gespeichert)
|
||||
content = File.ReadAllText(TOKEN_FILE, Encoding.UTF8)
|
||||
Else
|
||||
content = Encoding.UTF8.GetString(plain)
|
||||
End If
|
||||
|
||||
Dim s = content.Split({vbLf}, StringSplitOptions.None)
|
||||
If s.Length < 4 Then Return Nothing
|
||||
Return New TokenState With {
|
||||
.AccessToken = s(0),
|
||||
.RefreshToken = s(1),
|
||||
.AccessExpiryUtc = New DateTime(Long.Parse(s(2)), DateTimeKind.Utc),
|
||||
.RefreshExpiryUtc = New DateTime(Long.Parse(s(3)), DateTimeKind.Utc)
|
||||
}
|
||||
Catch
|
||||
Return Nothing
|
||||
End Try
|
||||
End Function
|
||||
|
||||
Private Shared Sub ClearToken()
|
||||
SyncLock _lockObj
|
||||
_ts = Nothing
|
||||
Try
|
||||
If File.Exists(TOKEN_FILE) Then File.Delete(TOKEN_FILE)
|
||||
Catch
|
||||
End Try
|
||||
End SyncLock
|
||||
End Sub
|
||||
|
||||
' -------------- Utilities --------------
|
||||
Private Shared Function UtcNow() As DateTime
|
||||
Return DateTime.UtcNow
|
||||
End Function
|
||||
|
||||
Private Shared Function IsAccessValid(ts As TokenState) As Boolean
|
||||
Return ts IsNot Nothing AndAlso Not String.IsNullOrEmpty(ts.AccessToken) AndAlso UtcNow() < ts.AccessExpiryUtc - SKEW
|
||||
End Function
|
||||
|
||||
Private Shared Function IsRefreshValid(ts As TokenState) As Boolean
|
||||
Return ts IsNot Nothing AndAlso Not String.IsNullOrEmpty(ts.RefreshToken) AndAlso UtcNow() < ts.RefreshExpiryUtc - SKEW
|
||||
End Function
|
||||
|
||||
' -------------- OAuth Flows --------------
|
||||
Private Shared Function PasswordLogin() As TokenState
|
||||
Dim http As New Chilkat.Http
|
||||
Dim req As New Chilkat.HttpRequest
|
||||
req.HttpVerb = "POST"
|
||||
req.Path = KC_TOKEN_PATH
|
||||
req.AddParam("grant_type", "password")
|
||||
req.AddParam("username", KC_USERNAME)
|
||||
req.AddParam("password", KC_PASSWORD)
|
||||
req.AddParam("client_id", KC_CLIENT_ID)
|
||||
req.AddParam("scope", "openid offline_access")
|
||||
req.AddHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
Dim resp = http.PostUrlEncoded(KC_BASE, req)
|
||||
If resp Is Nothing Then Throw New Exception("Token-Request fehlgeschlagen: " & http.LastErrorText)
|
||||
If resp.StatusCode <> 200 Then Throw New Exception("Password-Grant fehlgeschlagen: " & resp.StatusCode & " - " & resp.BodyStr)
|
||||
|
||||
Dim json As New Chilkat.JsonObject : json.Load(resp.BodyStr)
|
||||
Dim access = json.StringOf("access_token")
|
||||
Dim refresh = json.StringOf("refresh_token")
|
||||
Dim exp = Math.Max(60, json.IntOf("expires_in"))
|
||||
Dim rexp = Math.Max(300, json.IntOf("refresh_expires_in"))
|
||||
|
||||
Dim ts = New TokenState With {
|
||||
.AccessToken = access,
|
||||
.RefreshToken = refresh,
|
||||
.AccessExpiryUtc = UtcNow().AddSeconds(exp),
|
||||
.RefreshExpiryUtc = UtcNow().AddSeconds(rexp)
|
||||
}
|
||||
SaveTokenSecure(ts)
|
||||
Return ts
|
||||
End Function
|
||||
|
||||
Private Shared Function RefreshLogin(oldTs As TokenState) As TokenState
|
||||
If oldTs Is Nothing OrElse String.IsNullOrEmpty(oldTs.RefreshToken) Then
|
||||
Throw New Exception("Kein gültiger Refresh-Token vorhanden.")
|
||||
End If
|
||||
|
||||
Dim http As New Chilkat.Http
|
||||
Dim req As New Chilkat.HttpRequest
|
||||
req.HttpVerb = "POST"
|
||||
req.Path = KC_TOKEN_PATH
|
||||
req.AddParam("grant_type", "refresh_token")
|
||||
req.AddParam("refresh_token", oldTs.RefreshToken)
|
||||
req.AddParam("client_id", KC_CLIENT_ID)
|
||||
req.AddHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
' 2) Versuche Refresh, falls vorhanden
|
||||
If store IsNot Nothing AndAlso Not String.IsNullOrWhiteSpace(store.RefreshToken) Then
|
||||
store = TryRefresh(store.RefreshToken)
|
||||
End If
|
||||
|
||||
Dim resp = http.PostUrlEncoded(KC_BASE, req)
|
||||
If resp Is Nothing Then Throw New Exception("Refresh-Request fehlgeschlagen: " & http.LastErrorText)
|
||||
If resp.StatusCode <> 200 Then Throw New Exception("Refresh fehlgeschlagen: " & resp.StatusCode & " - " & resp.BodyStr)
|
||||
' 3) Fallback: Password-Grant Login
|
||||
If store Is Nothing OrElse String.IsNullOrWhiteSpace(store.AccessToken) Then
|
||||
store = PasswordLogin()
|
||||
End If
|
||||
|
||||
Dim json As New Chilkat.JsonObject : json.Load(resp.BodyStr)
|
||||
Dim access = json.StringOf("access_token")
|
||||
Dim refresh = json.StringOf("refresh_token") ' Rotation beachten
|
||||
Dim exp = Math.Max(60, json.IntOf("expires_in"))
|
||||
Dim rexp = Math.Max(300, json.IntOf("refresh_expires_in"))
|
||||
' Validierung
|
||||
If store Is Nothing OrElse String.IsNullOrWhiteSpace(store.AccessToken) Then
|
||||
Throw New ApplicationException("Konnte keinen gültigen Access Token erhalten (leer).")
|
||||
End If
|
||||
|
||||
Dim ts = New TokenState With {
|
||||
.AccessToken = access,
|
||||
.RefreshToken = refresh,
|
||||
.AccessExpiryUtc = UtcNow().AddSeconds(exp),
|
||||
.RefreshExpiryUtc = UtcNow().AddSeconds(rexp)
|
||||
' Persistieren & zurück
|
||||
SaveStore(store)
|
||||
Return store.AccessToken
|
||||
End Function
|
||||
|
||||
' ======= INTERNES =======
|
||||
Private Shared Function PasswordLogin() As TokenStore
|
||||
Dim form = New Dictionary(Of String, String) From {
|
||||
{"grant_type", "password"},
|
||||
{"username", USERNAME},
|
||||
{"password", PASSWORD},
|
||||
{"client_id", CLIENT_ID}
|
||||
}
|
||||
SaveTokenSecure(ts)
|
||||
Return ts
|
||||
|
||||
Dim resp = PostForm(form)
|
||||
Dim token = ParseTokenResponse(resp)
|
||||
|
||||
Return token
|
||||
End Function
|
||||
|
||||
' -------------- Public API --------------
|
||||
Public Shared Function GetValidAccessToken() As String
|
||||
SyncLock _lockObj
|
||||
If _ts Is Nothing Then _ts = LoadTokenSecure()
|
||||
Private Shared Function TryRefresh(refreshToken As String) As TokenStore
|
||||
Try
|
||||
Dim form = New Dictionary(Of String, String) From {
|
||||
{"grant_type", "refresh_token"},
|
||||
{"refresh_token", refreshToken},
|
||||
{"client_id", CLIENT_ID}
|
||||
}
|
||||
Dim resp = PostForm(form)
|
||||
Dim token = ParseTokenResponse(resp)
|
||||
|
||||
If IsAccessValid(_ts) Then
|
||||
Return _ts.AccessToken
|
||||
' Nur speichern, wenn ein Access Token vorhanden ist
|
||||
If Not String.IsNullOrWhiteSpace(token.AccessToken) Then
|
||||
SaveStore(token)
|
||||
Return token
|
||||
End If
|
||||
Catch ex As Exception
|
||||
' Ignorieren -> fällt auf PasswordLogin zurück
|
||||
End Try
|
||||
|
||||
If IsRefreshValid(_ts) Then
|
||||
Try
|
||||
_ts = RefreshLogin(_ts)
|
||||
Return _ts.AccessToken
|
||||
Catch
|
||||
' fällt durch auf PasswordLogin
|
||||
End Try
|
||||
End If
|
||||
|
||||
_ts = PasswordLogin()
|
||||
Return _ts.AccessToken
|
||||
End SyncLock
|
||||
Return Nothing
|
||||
End Function
|
||||
|
||||
Public Shared Sub ResetTokenCache()
|
||||
ClearToken()
|
||||
Private Shared Function PostForm(formFields As Dictionary(Of String, String)) As String
|
||||
Using client As New HttpClient()
|
||||
Using content As New FormUrlEncodedContent(formFields)
|
||||
Dim response = client.PostAsync(TOKEN_ENDPOINT, content).Result
|
||||
Dim body = response.Content.ReadAsStringAsync().Result
|
||||
|
||||
If Not response.IsSuccessStatusCode Then
|
||||
Throw New ApplicationException(
|
||||
$"Token-Endpoint Fehler ({CInt(response.StatusCode)}): {body}")
|
||||
End If
|
||||
|
||||
Return body
|
||||
End Using
|
||||
End Using
|
||||
End Function
|
||||
|
||||
Private Shared Function ParseTokenResponse(json As String) As TokenStore
|
||||
Dim r = JsonConvert.DeserializeObject(Of TokenResponse)(json)
|
||||
|
||||
If r Is Nothing OrElse String.IsNullOrWhiteSpace(r.access_token) Then
|
||||
Throw New ApplicationException("Token-Antwort ungültig oder ohne access_token.")
|
||||
End If
|
||||
|
||||
Dim now = DateTimeOffset.UtcNow
|
||||
Dim expiresIn = If(r.expires_in <= 0, 3600, r.expires_in) ' Fallback 1h
|
||||
|
||||
Dim store = New TokenStore With {
|
||||
.AccessToken = r.access_token.Trim(),
|
||||
.RefreshToken = If(r.refresh_token, String.Empty),
|
||||
.ExpiresAtUtc = now.AddSeconds(expiresIn)
|
||||
}
|
||||
|
||||
' === Konsolen-Ausgabe ===
|
||||
Console.WriteLine("== Neuer Token erhalten ==")
|
||||
Console.WriteLine("Access Token: " & store.AccessToken)
|
||||
Console.WriteLine("Refresh Token: " & store.RefreshToken)
|
||||
Console.WriteLine("Gültig bis UTC: " & store.ExpiresAtUtc.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
|
||||
Return store
|
||||
End Function
|
||||
|
||||
' ======= PERSISTENZ =======
|
||||
Private Shared Function LoadStore() As TokenStore
|
||||
Try
|
||||
If File.Exists(StorePath) Then
|
||||
Dim json = File.ReadAllText(StorePath, Encoding.UTF8)
|
||||
Dim s = JsonConvert.DeserializeObject(Of TokenStore)(json)
|
||||
' Ausgabe in Konsole
|
||||
If s IsNot Nothing Then
|
||||
Console.WriteLine("== Token aus Datei geladen ==")
|
||||
Console.WriteLine("Access Token: " & (If(String.IsNullOrWhiteSpace(s.AccessToken), "<leer>", s.AccessToken)))
|
||||
Console.WriteLine("Refresh Token: " & (If(String.IsNullOrWhiteSpace(s.RefreshToken), "<leer>", s.RefreshToken)))
|
||||
Console.WriteLine("Gültig bis UTC: " & s.ExpiresAtUtc.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
End If
|
||||
Return s
|
||||
Else
|
||||
' Datei existiert nicht -> Info ausgeben
|
||||
Console.WriteLine("Keine Token-Datei vorhanden, neuer Login erforderlich.")
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Console.WriteLine("Fehler beim Laden der Token-Datei: " & ex.Message)
|
||||
' Datei defekt? -> Ignorieren, neu holen
|
||||
End Try
|
||||
Return Nothing
|
||||
End Function
|
||||
|
||||
Private Shared Sub SaveStore(store As TokenStore)
|
||||
Try
|
||||
Dim dir = Path.GetDirectoryName(StorePath)
|
||||
If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir)
|
||||
|
||||
' Datei immer neu schreiben/überschreiben
|
||||
Dim json = JsonConvert.SerializeObject(store, Formatting.Indented)
|
||||
File.WriteAllText(StorePath, json, Encoding.UTF8)
|
||||
|
||||
Console.WriteLine("Token-Datei gespeichert: " & StorePath)
|
||||
Catch ex As Exception
|
||||
Console.WriteLine("Fehler beim Speichern der Token-Datei: " & ex.Message)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
End Class
|
||||
' ======= DTOs =======
|
||||
Private Class TokenResponse
|
||||
Public Property access_token As String
|
||||
Public Property expires_in As Integer
|
||||
Public Property refresh_expires_in As Integer
|
||||
Public Property refresh_token As String
|
||||
Public Property token_type As String
|
||||
Public Property scope As String
|
||||
' … weitere Felder bei Bedarf
|
||||
End Class
|
||||
|
||||
Private Class TokenStore
|
||||
Public Property AccessToken As String
|
||||
Public Property RefreshToken As String
|
||||
Public Property ExpiresAtUtc As DateTimeOffset
|
||||
End Class
|
||||
|
||||
End Class
|
||||
|
||||
Reference in New Issue
Block a user