162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v4"
|
|
"github.com/go-jose/go-jose/v4/jwt"
|
|
)
|
|
|
|
func TestGoogleProviderConfigConfigured(t *testing.T) {
|
|
empty := GoogleProviderConfig{}
|
|
if empty.Configured() {
|
|
t.Fatal("empty Google config must not be configured")
|
|
}
|
|
|
|
cfg := GoogleProviderConfig{
|
|
ClientID: "google-client",
|
|
ClientSecret: "google-secret",
|
|
RedirectURL: "https://xtablo.test/auth/google/callback",
|
|
}
|
|
if !cfg.Configured() {
|
|
t.Fatal("complete Google config must be configured")
|
|
}
|
|
}
|
|
|
|
func TestOAuthStateAndNonceCookiesValidateExactValue(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
SetOAuthCookie(rec, "google", OAuthCookieState, "state-value", false)
|
|
SetOAuthCookie(rec, "google", OAuthCookieNonce, "nonce-value", false)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/google/callback", nil)
|
|
for _, c := range rec.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
if !ValidateOAuthCookie(req, "google", OAuthCookieState, "state-value") {
|
|
t.Fatal("state cookie should validate matching value")
|
|
}
|
|
if ValidateOAuthCookie(req, "google", OAuthCookieState, "wrong-state") {
|
|
t.Fatal("state cookie should reject mismatched value")
|
|
}
|
|
if !ValidateOAuthCookie(req, "google", OAuthCookieNonce, "nonce-value") {
|
|
t.Fatal("nonce cookie should validate matching value")
|
|
}
|
|
if ValidateOAuthCookie(req, "google", OAuthCookieNonce, "wrong-nonce") {
|
|
t.Fatal("nonce cookie should reject mismatched value")
|
|
}
|
|
}
|
|
|
|
func TestOAuthCookieNameIncludesProviderAndKind(t *testing.T) {
|
|
if got := OAuthCookieName("google", OAuthCookieState); got != "xtablo_oauth_google_state" {
|
|
t.Fatalf("state cookie name = %q", got)
|
|
}
|
|
if got := OAuthCookieName("google", OAuthCookieNonce); got != "xtablo_oauth_google_nonce" {
|
|
t.Fatalf("nonce cookie name = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAppleProviderConfigConfigured(t *testing.T) {
|
|
empty := AppleProviderConfig{}
|
|
if empty.Configured() {
|
|
t.Fatal("empty Apple config must not be configured")
|
|
}
|
|
|
|
cfg := AppleProviderConfig{
|
|
ClientID: "com.xtablo.web",
|
|
TeamID: "TEAMID1234",
|
|
KeyID: "KEYID1234",
|
|
PrivateKey: testApplePrivateKeyPEM(t),
|
|
RedirectURL: "https://xtablo.test/auth/apple/callback",
|
|
}
|
|
if !cfg.Configured() {
|
|
t.Fatal("complete Apple config must be configured")
|
|
}
|
|
}
|
|
|
|
func TestAppleClientSecretClaimsAndKeyID(t *testing.T) {
|
|
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
privateKeyPEM := testApplePrivateKeyPEM(t)
|
|
cfg := AppleProviderConfig{
|
|
ClientID: "com.xtablo.web",
|
|
TeamID: "TEAMID1234",
|
|
KeyID: "KEYID1234",
|
|
PrivateKey: stringsWithEscapedNewlines(privateKeyPEM),
|
|
RedirectURL: "https://xtablo.test/auth/apple/callback",
|
|
}
|
|
|
|
secret, err := cfg.ClientSecret(now)
|
|
if err != nil {
|
|
t.Fatalf("ClientSecret: %v", err)
|
|
}
|
|
key := parseApplePrivateKeyForTest(t, privateKeyPEM)
|
|
parsed, err := jwt.ParseSigned(secret, []jose.SignatureAlgorithm{jose.ES256})
|
|
if err != nil {
|
|
t.Fatalf("ParseSigned: %v", err)
|
|
}
|
|
var claims jwt.Claims
|
|
if err := parsed.Claims(&key.PublicKey, &claims); err != nil {
|
|
t.Fatalf("Claims: %v", err)
|
|
}
|
|
if claims.Issuer != "TEAMID1234" {
|
|
t.Fatalf("iss = %q", claims.Issuer)
|
|
}
|
|
if claims.Subject != "com.xtablo.web" {
|
|
t.Fatalf("sub = %q", claims.Subject)
|
|
}
|
|
if len(claims.Audience) != 1 || claims.Audience[0] != "https://appleid.apple.com" {
|
|
t.Fatalf("aud = %#v", claims.Audience)
|
|
}
|
|
if !claims.Expiry.Time().After(now) {
|
|
t.Fatalf("exp = %s; want after %s", claims.Expiry.Time(), now)
|
|
}
|
|
if parsed.Headers[0].KeyID != "KEYID1234" {
|
|
t.Fatalf("kid = %q", parsed.Headers[0].KeyID)
|
|
}
|
|
}
|
|
|
|
func testApplePrivateKeyPEM(t *testing.T) string {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey: %v", err)
|
|
}
|
|
der, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
t.Fatalf("MarshalECPrivateKey: %v", err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}))
|
|
}
|
|
|
|
func parseApplePrivateKeyForTest(t *testing.T, privateKeyPEM string) *ecdsa.PrivateKey {
|
|
t.Helper()
|
|
block, _ := pem.Decode([]byte(privateKeyPEM))
|
|
if block == nil {
|
|
t.Fatal("missing PEM block")
|
|
}
|
|
key, err := x509.ParseECPrivateKey(block.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("ParseECPrivateKey: %v", err)
|
|
}
|
|
return key
|
|
}
|
|
|
|
func stringsWithEscapedNewlines(value string) string {
|
|
out := ""
|
|
for _, r := range value {
|
|
if r == '\n' {
|
|
out += `\n`
|
|
} else {
|
|
out += string(r)
|
|
}
|
|
}
|
|
return out
|
|
}
|