Add native NixOS deployment and UI-managed provider credentials

This commit is contained in:
Lars Nolden
2026-09-10 14:25:37 +02:00
parent 9843fe0c50
commit 964b9dfc15
21 changed files with 2084 additions and 104 deletions
+60 -7
View File
@@ -1,6 +1,7 @@
package banking
import (
"bytes"
"context"
"crypto"
"crypto/rand"
@@ -14,8 +15,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -27,11 +26,8 @@ func testProvider(t *testing.T, handler http.HandlerFunc) (*EnableBanking, *rsa.
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "private.pem")
if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}), 0600); err != nil {
t.Fatal(err)
}
p, err := NewEnableBanking("test-app", path, "http://localhost:8080/api/banking/callback")
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
p, err := NewEnableBanking("test-app", keyPEM, "http://localhost:8080/api/banking/callback")
if err != nil {
t.Fatal(err)
}
@@ -268,3 +264,60 @@ func TestEnableBankingExpiredConsentRequiresReconnect(t *testing.T) {
t.Fatalf("expired consent must request reconnection: %v", err)
}
}
func TestEnableBankingValidatesUploadedCredentials(t *testing.T) {
_, key := testProvider(t, func(w http.ResponseWriter, r *http.Request) {
t.Error("credential validation must not call provider")
})
pkcs1 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
der, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatal(err)
}
pkcs8 := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
for _, content := range [][]byte{pkcs1, pkcs8} {
p, err := NewEnableBanking("test-app", content, "https://finance.example/api/banking/callback")
if err != nil {
t.Fatal(err)
}
token, err := p.jwt()
if err != nil {
t.Fatal(err)
}
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Authorization", "Bearer "+token)
assertJWT(t, r, key)
}
weak, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
t.Fatal(err)
}
for name, content := range map[string][]byte{
"invalid": []byte("secret-invalid-key"),
"oversized": bytes.Repeat([]byte("k"), MaxPrivateKeyPEM+1),
"multiple": append(append([]byte{}, pkcs1...), pkcs8...),
"prefix": append([]byte("secret-prefix\n"), pkcs1...),
"weak": pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(weak)}),
} {
t.Run(name, func(t *testing.T) {
if _, err := NewEnableBanking("test-app", content, "https://finance.example/api/banking/callback"); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatal("invalid PEM accepted or leaked")
}
})
}
for _, appID := range []string{"", "app one", "app\none", "app\u007fone", strings.Repeat("a", 257)} {
if _, err := NewEnableBanking(appID, pkcs1, "https://finance.example/api/banking/callback"); err == nil {
t.Fatal("invalid app ID accepted")
}
}
for _, redirect := range []string{
"https://finance.example/", "https://finance.example/api/banking/callback?secret=value",
"https://finance.example/api/banking/callback#", "https://finance.example/api/banking/callback?",
"https://user:secret@finance.example/api/banking/callback", "ftp://finance.example/api/banking/callback",
"https://finance.example/api/banking/%63allback", "https:///api/banking/callback",
} {
if _, err := NewEnableBanking("test-app", pkcs1, redirect); err == nil || strings.Contains(err.Error(), "secret") {
t.Fatal("invalid callback accepted or leaked")
}
}
}