Files
pocket-id/backend/internal/auditlogs/known_browser_test.go
T

90 lines
3.1 KiB
Go

package auditlogs
import (
"testing"
"time"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/stretchr/testify/require"
jwkutils "github.com/pocket-id/pocket-id/backend/internal/utils/jwk"
)
type browserTokenTestSigner struct{ key jwk.Key }
func (s browserTokenTestSigner) SignSessionToken(token jwt.Token) (string, error) {
signed, err := jwt.Sign(token, jwt.WithKey(jwkutils.SessionKeyAlg(), s.key))
return string(signed), err
}
func (s browserTokenTestSigner) VerifySessionToken(token string, options ...jwt.ValidateOption) (jwt.Token, error) {
parseOptions := []jwt.ParseOption{jwt.WithKey(jwkutils.SessionKeyAlg(), s.key), jwt.WithValidate(true)}
for _, option := range options {
parseOptions = append(parseOptions, option)
}
return jwt.ParseString(token, parseOptions...)
}
func newBrowserTokenService(t *testing.T) *browserTokens {
t.Helper()
key, err := jwkutils.GenerateSessionKey()
require.NoError(t, err)
return &browserTokens{signer: browserTokenTestSigner{key: key}, appURL: "https://test.example.com"}
}
func TestKnownBrowserToken(t *testing.T) {
s := newBrowserTokenService(t)
const lifetime = 180 * 24 * time.Hour
encoded, err := s.generate("user", lifetime)
require.NoError(t, err)
require.NoError(t, s.verify(encoded, "user"))
require.Error(t, s.verify(encoded, "other-user"))
require.Error(t, s.verify(encoded, ""))
token, err := jwt.ParseString(encoded, jwt.WithKey(jwkutils.SessionKeyAlg(), s.signer.(browserTokenTestSigner).key))
require.NoError(t, err)
issued, ok := token.IssuedAt()
require.True(t, ok)
expires, ok := token.Expiration()
require.True(t, ok)
require.Equal(t, lifetime, expires.Sub(issued))
}
func TestKnownBrowserTokenRejectsInvalidClaims(t *testing.T) {
s := newBrowserTokenService(t)
for _, tt := range []struct {
name string
claim string
value any
}{
{"expired", jwt.ExpirationKey, time.Now().Add(-time.Second)},
{"missing expiry", jwt.ExpirationKey, nil},
{"missing issued at", jwt.IssuedAtKey, nil},
{"future issued at", jwt.IssuedAtKey, time.Now().Add(time.Hour)},
{"wrong issuer", jwt.IssuerKey, "https://other.example.com"},
{"missing issuer", jwt.IssuerKey, nil},
{"wrong audience", jwt.AudienceKey, []string{"other"}},
{"missing audience", jwt.AudienceKey, nil},
{"missing user", jwt.SubjectKey, nil},
{"wrong purpose", "type", "access-token"},
{"missing purpose", "type", nil},
} {
t.Run(tt.name, func(t *testing.T) {
// Sign invalid claims with the real key so validation, rather than signature failure, rejects them
token, err := jwt.NewBuilder().Subject("user").Issuer(s.appURL).
Audience([]string{s.appURL}).IssuedAt(time.Now()).Expiration(time.Now().Add(time.Hour)).
Claim("type", knownBrowserJWTType).Build()
require.NoError(t, err)
if tt.value == nil {
require.NoError(t, token.Remove(tt.claim))
} else {
require.NoError(t, token.Set(tt.claim, tt.value))
}
encoded, err := jwt.Sign(token, jwt.WithKey(jwkutils.SessionKeyAlg(), s.signer.(browserTokenTestSigner).key))
require.NoError(t, err)
require.Error(t, s.verify(string(encoded), "user"))
})
}
}