init push
This commit is contained in:
257
internal/auth/handler_logout_test.go
Normal file
257
internal/auth/handler_logout_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"wucher/internal/config"
|
||||
)
|
||||
|
||||
func TestHandleBrowserLocalLogout_RedirectAndClearSessionCookie(t *testing.T) {
|
||||
prev := SAMLMiddleware
|
||||
SAMLMiddleware = nil
|
||||
t.Cleanup(func() { SAMLMiddleware = prev })
|
||||
|
||||
h := NewHandler(config.Config{
|
||||
FrontendURL: "https://wfm-dev.mybit.co.id",
|
||||
CookieDomain: ".mybit.co.id",
|
||||
CookieSecure: true,
|
||||
}, nil)
|
||||
|
||||
app := fiber.New()
|
||||
app.Get("/api/v1/auth/microsoft/logout", h.HandleBrowserLocalLogout)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/microsoft/logout", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "https://wfm-dev.mybit.co.id/signin?loggedOut=true" {
|
||||
t.Fatalf("unexpected redirect: %s", got)
|
||||
}
|
||||
|
||||
setCookie := strings.Join(resp.Header.Values("Set-Cookie"), "\n")
|
||||
if !strings.Contains(setCookie, "session_token=") {
|
||||
t.Fatalf("expected session_token clear cookie, got %q", setCookie)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(setCookie), "domain=.mybit.co.id") {
|
||||
t.Fatalf("expected cookie domain .mybit.co.id, got %q", setCookie)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRedirectSignaturePayload_PreservesRawEncoding(t *testing.T) {
|
||||
raw := "SAMLResponse=abc%2B123%3D%3D&RelayState=x%2By%252Fz&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=ignored"
|
||||
got, err := buildRedirectSignaturePayload(raw, "SAMLResponse")
|
||||
if err != nil {
|
||||
t.Fatalf("build payload: %v", err)
|
||||
}
|
||||
want := "SAMLResponse=abc%2B123%3D%3D&RelayState=x%2By%252Fz&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256"
|
||||
if string(got) != want {
|
||||
t.Fatalf("payload mismatch\nwant: %s\ngot: %s", want, string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSAMLLogoutResponse_GetRedirectSignature_Valid_WithRelayState(t *testing.T) {
|
||||
runValidSAMLResponseRedirectTest(t, "relay-state-1", true)
|
||||
}
|
||||
|
||||
func TestHandleSAMLLogoutResponse_GetRedirectSignature_Valid_WithoutRelayState(t *testing.T) {
|
||||
runValidSAMLResponseRedirectTest(t, "", false)
|
||||
}
|
||||
|
||||
func TestHandleSAMLLogoutResponse_GetRedirectSignature_Invalid(t *testing.T) {
|
||||
priv, cert := testSigningCert(t)
|
||||
setTestSAMLMiddlewareWithCert(t, cert, "https://sts.windows.net/tenant/")
|
||||
|
||||
xmlB64 := base64.StdEncoding.EncodeToString([]byte(testLogoutResponseXML("https://sts.windows.net/tenant/")))
|
||||
sigAlg := "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
params := []string{
|
||||
"SAMLResponse=" + url.QueryEscape(xmlB64),
|
||||
"SigAlg=" + url.QueryEscape(sigAlg),
|
||||
}
|
||||
canonical := strings.Join(params, "&")
|
||||
sig := signRedirectPayloadRSA256(t, priv, []byte(canonical))
|
||||
// Break signature on purpose
|
||||
sig = strings.TrimRight(sig, "=") + "A"
|
||||
|
||||
path := "/api/v1/auth/microsoft/logout?" + canonical + "&Signature=" + url.QueryEscape(sig)
|
||||
h := NewHandler(config.Config{FrontendURL: "https://wfm-dev.mybit.co.id"}, nil)
|
||||
app := fiber.New()
|
||||
app.Get("/api/v1/auth/microsoft/logout", h.HandleSAMLLogoutResponse)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSAMLLogoutResponse_GetMissingSignature(t *testing.T) {
|
||||
_, cert := testSigningCert(t)
|
||||
setTestSAMLMiddlewareWithCert(t, cert, "https://sts.windows.net/tenant/")
|
||||
|
||||
xmlB64 := base64.StdEncoding.EncodeToString([]byte(testLogoutResponseXML("https://sts.windows.net/tenant/")))
|
||||
path := "/api/v1/auth/microsoft/logout?SAMLResponse=" + url.QueryEscape(xmlB64)
|
||||
|
||||
h := NewHandler(config.Config{FrontendURL: "https://wfm-dev.mybit.co.id"}, nil)
|
||||
app := fiber.New()
|
||||
app.Get("/api/v1/auth/microsoft/logout", h.HandleSAMLLogoutResponse)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func runValidSAMLResponseRedirectTest(t *testing.T, relayState string, withRelay bool) {
|
||||
t.Helper()
|
||||
priv, cert := testSigningCert(t)
|
||||
setTestSAMLMiddlewareWithCert(t, cert, "https://sts.windows.net/tenant/")
|
||||
|
||||
xmlB64 := base64.StdEncoding.EncodeToString([]byte(testLogoutResponseXML("https://sts.windows.net/tenant/")))
|
||||
sigAlg := "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
|
||||
parts := []string{"SAMLResponse=" + url.QueryEscape(xmlB64)}
|
||||
if withRelay {
|
||||
parts = append(parts, "RelayState="+url.QueryEscape(relayState))
|
||||
}
|
||||
parts = append(parts, "SigAlg="+url.QueryEscape(sigAlg))
|
||||
canonical := strings.Join(parts, "&")
|
||||
sig := signRedirectPayloadRSA256(t, priv, []byte(canonical))
|
||||
rawQuery := canonical + "&Signature=" + url.QueryEscape(sig)
|
||||
|
||||
h := NewHandler(config.Config{FrontendURL: "https://wfm-dev.mybit.co.id"}, nil)
|
||||
app := fiber.New()
|
||||
app.Get("/api/v1/auth/microsoft/logout", h.HandleSAMLLogoutResponse)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/microsoft/logout?"+rawQuery, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("expected 302, got %d", resp.StatusCode)
|
||||
}
|
||||
if got := resp.Header.Get("Location"); got != "https://wfm-dev.mybit.co.id/signin?loggedOut=true" {
|
||||
t.Fatalf("unexpected redirect: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func signRedirectPayloadRSA256(t *testing.T, priv *rsa.PrivateKey, payload []byte) string {
|
||||
t.Helper()
|
||||
sum := sha256.Sum256(payload)
|
||||
sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, sum[:])
|
||||
if err != nil {
|
||||
t.Fatalf("sign payload: %v", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(sig)
|
||||
}
|
||||
|
||||
func testSigningCert(t *testing.T) (*rsa.PrivateKey, *x509.Certificate) {
|
||||
t.Helper()
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("gen key: %v", err)
|
||||
}
|
||||
serial, _ := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||
tpl := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{CommonName: "test-idp"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatalf("create cert: %v", err)
|
||||
}
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
t.Fatalf("parse cert: %v", err)
|
||||
}
|
||||
return priv, cert
|
||||
}
|
||||
|
||||
func setTestSAMLMiddlewareWithCert(t *testing.T, cert *x509.Certificate, entityID string) {
|
||||
t.Helper()
|
||||
prev := SAMLMiddleware
|
||||
SAMLMiddleware = &samlsp.Middleware{}
|
||||
SAMLMiddleware.ServiceProvider.IDPMetadata = &saml.EntityDescriptor{
|
||||
EntityID: entityID,
|
||||
IDPSSODescriptors: []saml.IDPSSODescriptor{
|
||||
{
|
||||
SSODescriptor: saml.SSODescriptor{
|
||||
RoleDescriptor: saml.RoleDescriptor{
|
||||
KeyDescriptors: []saml.KeyDescriptor{
|
||||
{
|
||||
KeyInfo: dsigKeyInfoFromCert(cert),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Cleanup(func() { SAMLMiddleware = prev })
|
||||
}
|
||||
|
||||
func dsigKeyInfoFromCert(cert *x509.Certificate) saml.KeyInfo {
|
||||
if cert == nil {
|
||||
return saml.KeyInfo{}
|
||||
}
|
||||
return saml.KeyInfo{
|
||||
X509Data: saml.X509Data{
|
||||
X509Certificates: []saml.X509Certificate{
|
||||
{Data: base64.StdEncoding.EncodeToString(cert.Raw)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testLogoutResponseXML(issuer string) string {
|
||||
return fmt.Sprintf(
|
||||
`<LogoutResponse xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_id123" Version="2.0" IssueInstant="2026-05-28T00:00:00Z"><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">%s</Issuer><Status><StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></Status></LogoutResponse>`,
|
||||
issuer,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCertificateThumbprintSHA1(t *testing.T) {
|
||||
_, cert := testSigningCert(t)
|
||||
got := certificateThumbprintSHA1(cert)
|
||||
if got == "" {
|
||||
t.Fatal("thumbprint should not be empty")
|
||||
}
|
||||
sum := sha1.Sum(cert.Raw)
|
||||
want := strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||
if got != want {
|
||||
t.Fatalf("thumbprint mismatch: got=%s want=%s", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user