2023-11-21 12:11:38 +00:00
package oidc
import (
"context"
"errors"
"testing"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
type publicKey struct {
id string
alg string
use domain . KeyUsage
seq uint64
expiry time . Time
key any
}
func ( k * publicKey ) ID ( ) string {
return k . id
}
func ( k * publicKey ) Algorithm ( ) string {
return k . alg
}
func ( k * publicKey ) Use ( ) domain . KeyUsage {
return k . use
}
func ( k * publicKey ) Sequence ( ) uint64 {
return k . seq
}
func ( k * publicKey ) Expiry ( ) time . Time {
return k . expiry
}
func ( k * publicKey ) Key ( ) any {
return k . key
}
var (
clock = clockwork . NewFakeClock ( )
keyDB = map [ string ] * publicKey {
"key1" : {
id : "key1" ,
alg : "alg" ,
use : domain . KeyUsageSigning ,
seq : 1 ,
expiry : clock . Now ( ) . Add ( time . Minute ) ,
} ,
"key2" : {
id : "key2" ,
alg : "alg" ,
use : domain . KeyUsageSigning ,
seq : 3 ,
expiry : clock . Now ( ) . Add ( 10 * time . Hour ) ,
} ,
2024-01-29 15:11:52 +00:00
"exp1" : {
id : "key2" ,
alg : "alg" ,
use : domain . KeyUsageSigning ,
seq : 4 ,
expiry : clock . Now ( ) . Add ( - time . Hour ) ,
} ,
2023-11-21 12:11:38 +00:00
}
)
2024-01-29 15:11:52 +00:00
func queryKeyDB ( _ context . Context , keyID string ) ( query . PublicKey , error ) {
2023-11-21 12:11:38 +00:00
if key , ok := keyDB [ keyID ] ; ok {
return key , nil
}
return nil , errors . New ( "not found" )
}
2024-01-29 15:11:52 +00:00
func Test_publicKeyCache ( t * testing . T ) {
2023-11-21 12:11:38 +00:00
background , cancel := context . WithCancel (
clockwork . AddToContext ( context . Background ( ) , clock ) ,
)
defer cancel ( )
2024-01-29 15:11:52 +00:00
// create an empty cache with a purge go routine, runs every minute.
// keys are cached for at least 1 Hour after last use.
cache := newPublicKeyCache ( background , time . Hour , queryKeyDB )
2023-11-21 12:11:38 +00:00
ctx := authz . NewMockContext ( "instanceID" , "orgID" , "userID" )
// query error
2024-01-29 15:11:52 +00:00
_ , err := cache . getKey ( ctx , "key9" )
2023-11-21 12:11:38 +00:00
require . Error ( t , err )
// get key first time, populate the cache
2024-01-29 15:11:52 +00:00
got , err := cache . getKey ( ctx , "key1" )
2023-11-21 12:11:38 +00:00
require . NoError ( t , err )
2024-01-29 15:11:52 +00:00
require . NotNil ( t , got )
assert . Equal ( t , keyDB [ "key1" ] , got . PublicKey )
2023-11-21 12:11:38 +00:00
// move time forward
2024-01-29 15:11:52 +00:00
clock . Advance ( 15 * time . Minute )
2023-11-21 12:11:38 +00:00
time . Sleep ( time . Millisecond )
// key should still be in cache
2024-01-29 15:11:52 +00:00
cache . mtx . RLock ( )
_ , ok := cache . instanceKeys [ "instanceID" ] [ "key1" ]
2023-11-21 12:11:38 +00:00
require . True ( t , ok )
2024-01-29 15:11:52 +00:00
cache . mtx . RUnlock ( )
2023-11-21 12:11:38 +00:00
2024-01-29 15:11:52 +00:00
// move time forward
clock . Advance ( 50 * time . Minute )
time . Sleep ( time . Millisecond )
2023-11-21 12:11:38 +00:00
// get the second key from DB
2024-01-29 15:11:52 +00:00
got , err = cache . getKey ( ctx , "key2" )
2023-11-21 12:11:38 +00:00
require . NoError ( t , err )
2024-01-29 15:11:52 +00:00
require . NotNil ( t , got )
assert . Equal ( t , keyDB [ "key2" ] , got . PublicKey )
2023-11-21 12:11:38 +00:00
// move time forward
2024-01-29 15:11:52 +00:00
clock . Advance ( 15 * time . Minute )
2023-11-21 12:11:38 +00:00
time . Sleep ( time . Millisecond )
2024-01-29 15:11:52 +00:00
// first key should be purged, second still present
cache . mtx . RLock ( )
_ , ok = cache . instanceKeys [ "instanceID" ] [ "key1" ]
2023-11-21 12:11:38 +00:00
require . False ( t , ok )
2024-01-29 15:11:52 +00:00
_ , ok = cache . instanceKeys [ "instanceID" ] [ "key2" ]
2023-11-21 12:11:38 +00:00
require . True ( t , ok )
2024-01-29 15:11:52 +00:00
cache . mtx . RUnlock ( )
2023-11-21 12:11:38 +00:00
// get the second key from cache
2024-01-29 15:11:52 +00:00
got , err = cache . getKey ( ctx , "key2" )
2023-11-21 12:11:38 +00:00
require . NoError ( t , err )
2024-01-29 15:11:52 +00:00
require . NotNil ( t , got )
assert . Equal ( t , keyDB [ "key2" ] , got . PublicKey )
2023-11-21 12:11:38 +00:00
// move time forward
2024-01-29 15:11:52 +00:00
clock . Advance ( 2 * time . Hour )
2023-11-21 12:11:38 +00:00
time . Sleep ( time . Millisecond )
// now the cache should be empty
2024-01-29 15:11:52 +00:00
cache . mtx . RLock ( )
assert . Empty ( t , cache . instanceKeys )
cache . mtx . RUnlock ( )
2023-11-21 12:11:38 +00:00
}
2024-01-29 15:11:52 +00:00
func Test_oidcKeySet_VerifySignature ( t * testing . T ) {
2023-11-21 12:11:38 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2024-01-29 15:11:52 +00:00
cache := newPublicKeyCache ( ctx , time . Second , queryKeyDB )
2023-11-21 12:11:38 +00:00
tests := [ ] struct {
name string
2024-01-29 15:11:52 +00:00
opts [ ] keySetOption
2023-11-21 12:11:38 +00:00
jws * jose . JSONWebSignature
} {
{
name : "invalid token" ,
jws : & jose . JSONWebSignature { } ,
} ,
{
name : "key not found" ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "xxx" ,
} ,
} } ,
} ,
} ,
{
name : "verify error" ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "key1" ,
} ,
} } ,
} ,
} ,
2024-01-29 15:11:52 +00:00
{
name : "expired, no check" ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "exp1" ,
} ,
} } ,
} ,
} ,
{
name : "expired, with check" ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "exp1" ,
} ,
} } ,
} ,
opts : [ ] keySetOption {
withKeyExpiryCheck ( true ) ,
} ,
} ,
2023-11-21 12:11:38 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2024-01-29 15:11:52 +00:00
k := newOidcKeySet ( cache , tt . opts ... )
2023-11-21 12:11:38 +00:00
_ , err := k . VerifySignature ( ctx , tt . jws )
require . Error ( t , err )
} )
}
}
func Test_keySetMap_VerifySignature ( t * testing . T ) {
tests := [ ] struct {
name string
k keySetMap
jws * jose . JSONWebSignature
} {
{
name : "invalid signature" ,
k : keySetMap {
"key1" : [ ] byte ( "foo" ) ,
} ,
jws : & jose . JSONWebSignature { } ,
} ,
{
name : "parse error" ,
k : keySetMap {
"key1" : [ ] byte ( "foo" ) ,
} ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "key1" ,
} ,
} } ,
} ,
} ,
{
name : "verify error" ,
k : keySetMap {
"key1" : [ ] byte ( "-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsvX9P58JFxEs5C+L+H7W\nduFSWL5EPzber7C2m94klrSV6q0bAcrYQnGwFOlveThsY200hRbadKaKjHD7qIKH\nDEe0IY2PSRht33Jye52AwhkRw+M3xuQH/7R8LydnsNFk2KHpr5X2SBv42e37LjkE\nslKSaMRgJW+v0KZ30piY8QsdFRKKaVg5/Ajt1YToM1YVsdHXJ3vmXFMtypLdxwUD\ndIaLEX6pFUkU75KSuEQ/E2luT61Q3ta9kOWm9+0zvi7OMcbdekJT7mzcVnh93R1c\n13ZhQCLbh9A7si8jKFtaMWevjayrvqQABEcTN9N4Hoxcyg6l4neZtRDk75OMYcqm\nDQIDAQAB\n-----END RSA PUBLIC KEY-----\n" ) ,
} ,
jws : & jose . JSONWebSignature {
Signatures : [ ] jose . Signature { {
Header : jose . Header {
KeyID : "key1" ,
} ,
} } ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
_ , err := tt . k . VerifySignature ( context . Background ( ) , tt . jws )
require . Error ( t , err )
} )
}
}