2023-11-21 14:11:38 +02:00
package oidc
import (
"context"
2024-08-23 15:43:46 +03:00
"crypto/rand"
2023-11-21 14:11:38 +02:00
"errors"
"testing"
"time"
2024-04-15 12:17:36 +03:00
"github.com/go-jose/go-jose/v4"
2023-11-21 14:11:38 +02:00
"github.com/jonboulle/clockwork"
2024-08-23 15:43:46 +03:00
"github.com/muhlemmer/gu"
2023-11-21 14:11:38 +02:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
2024-08-14 17:18:14 +03:00
"github.com/zitadel/zitadel/internal/crypto"
2023-11-21 14:11:38 +02:00
"github.com/zitadel/zitadel/internal/query"
)
type publicKey struct {
id string
alg string
2024-08-14 17:18:14 +03:00
use crypto . KeyUsage
2023-11-21 14:11:38 +02:00
seq uint64
expiry time . Time
key any
}
func ( k * publicKey ) ID ( ) string {
return k . id
}
func ( k * publicKey ) Algorithm ( ) string {
return k . alg
}
2024-08-14 17:18:14 +03:00
func ( k * publicKey ) Use ( ) crypto . KeyUsage {
2023-11-21 14:11:38 +02:00
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 ( )
2024-08-23 15:43:46 +03:00
keyDB = map [ string ] struct {
webKey * jose . JSONWebKey
expiry * time . Time
} {
2023-11-21 14:11:38 +02:00
"key1" : {
2024-08-23 15:43:46 +03:00
webKey : & jose . JSONWebKey {
Key : "abc" ,
KeyID : "key1" ,
Algorithm : "alg" ,
Use : "sig" ,
} ,
expiry : gu . Ptr ( clock . Now ( ) . Add ( time . Minute ) ) ,
2023-11-21 14:11:38 +02:00
} ,
"key2" : {
2024-08-23 15:43:46 +03:00
webKey : & jose . JSONWebKey {
Key : "def" ,
KeyID : "key1" ,
Algorithm : "alg" ,
Use : "sig" ,
} ,
expiry : gu . Ptr ( clock . Now ( ) . Add ( 10 * time . Hour ) ) ,
2023-11-21 14:11:38 +02:00
} ,
2024-01-29 17:11:52 +02:00
"exp1" : {
2024-08-23 15:43:46 +03:00
webKey : & jose . JSONWebKey {
Key : "ghi" ,
KeyID : "exp1" ,
Algorithm : "alg" ,
Use : "sig" ,
} ,
expiry : gu . Ptr ( clock . Now ( ) . Add ( - time . Hour ) ) ,
2024-01-29 17:11:52 +02:00
} ,
2023-11-21 14:11:38 +02:00
}
)
2024-08-23 15:43:46 +03:00
func queryKeyDB ( _ context . Context , keyID string ) ( * jose . JSONWebKey , * time . Time , error ) {
2023-11-21 14:11:38 +02:00
if key , ok := keyDB [ keyID ] ; ok {
2024-08-23 15:43:46 +03:00
return key . webKey , key . expiry , nil
2023-11-21 14:11:38 +02:00
}
2024-08-23 15:43:46 +03:00
return nil , nil , errors . New ( "not found" )
2023-11-21 14:11:38 +02:00
}
2024-01-29 17:11:52 +02:00
func Test_publicKeyCache ( t * testing . T ) {
2023-11-21 14:11:38 +02:00
background , cancel := context . WithCancel (
clockwork . AddToContext ( context . Background ( ) , clock ) ,
)
defer cancel ( )
2024-01-29 17:11:52 +02: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 14:11:38 +02:00
ctx := authz . NewMockContext ( "instanceID" , "orgID" , "userID" )
// query error
2024-01-29 17:11:52 +02:00
_ , err := cache . getKey ( ctx , "key9" )
2023-11-21 14:11:38 +02:00
require . Error ( t , err )
// get key first time, populate the cache
2024-01-29 17:11:52 +02:00
got , err := cache . getKey ( ctx , "key1" )
2023-11-21 14:11:38 +02:00
require . NoError ( t , err )
2024-01-29 17:11:52 +02:00
require . NotNil ( t , got )
2024-08-23 15:43:46 +03:00
assert . Equal ( t , keyDB [ "key1" ] . webKey , got . webKey )
2023-11-21 14:11:38 +02:00
// move time forward
2024-01-29 17:11:52 +02:00
clock . Advance ( 15 * time . Minute )
2023-11-21 14:11:38 +02:00
time . Sleep ( time . Millisecond )
// key should still be in cache
2024-01-29 17:11:52 +02:00
cache . mtx . RLock ( )
_ , ok := cache . instanceKeys [ "instanceID" ] [ "key1" ]
2023-11-21 14:11:38 +02:00
require . True ( t , ok )
2024-01-29 17:11:52 +02:00
cache . mtx . RUnlock ( )
2023-11-21 14:11:38 +02:00
2024-01-29 17:11:52 +02:00
// move time forward
clock . Advance ( 50 * time . Minute )
time . Sleep ( time . Millisecond )
2023-11-21 14:11:38 +02:00
// get the second key from DB
2024-01-29 17:11:52 +02:00
got , err = cache . getKey ( ctx , "key2" )
2023-11-21 14:11:38 +02:00
require . NoError ( t , err )
2024-01-29 17:11:52 +02:00
require . NotNil ( t , got )
2024-08-23 15:43:46 +03:00
assert . Equal ( t , keyDB [ "key2" ] . webKey , got . webKey )
2023-11-21 14:11:38 +02:00
// move time forward
2024-01-29 17:11:52 +02:00
clock . Advance ( 15 * time . Minute )
2023-11-21 14:11:38 +02:00
time . Sleep ( time . Millisecond )
2024-01-29 17:11:52 +02:00
// first key should be purged, second still present
cache . mtx . RLock ( )
_ , ok = cache . instanceKeys [ "instanceID" ] [ "key1" ]
2023-11-21 14:11:38 +02:00
require . False ( t , ok )
2024-01-29 17:11:52 +02:00
_ , ok = cache . instanceKeys [ "instanceID" ] [ "key2" ]
2023-11-21 14:11:38 +02:00
require . True ( t , ok )
2024-01-29 17:11:52 +02:00
cache . mtx . RUnlock ( )
2023-11-21 14:11:38 +02:00
// get the second key from cache
2024-01-29 17:11:52 +02:00
got , err = cache . getKey ( ctx , "key2" )
2023-11-21 14:11:38 +02:00
require . NoError ( t , err )
2024-01-29 17:11:52 +02:00
require . NotNil ( t , got )
2024-08-23 15:43:46 +03:00
assert . Equal ( t , keyDB [ "key2" ] . webKey , got . webKey )
2023-11-21 14:11:38 +02:00
// move time forward
2024-01-29 17:11:52 +02:00
clock . Advance ( 2 * time . Hour )
2023-11-21 14:11:38 +02:00
time . Sleep ( time . Millisecond )
// now the cache should be empty
2024-01-29 17:11:52 +02:00
cache . mtx . RLock ( )
assert . Empty ( t , cache . instanceKeys )
cache . mtx . RUnlock ( )
2023-11-21 14:11:38 +02:00
}
2024-01-29 17:11:52 +02:00
func Test_oidcKeySet_VerifySignature ( t * testing . T ) {
2023-11-21 14:11:38 +02:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
2024-01-29 17:11:52 +02:00
cache := newPublicKeyCache ( ctx , time . Second , queryKeyDB )
2023-11-21 14:11:38 +02:00
tests := [ ] struct {
name string
2024-01-29 17:11:52 +02:00
opts [ ] keySetOption
2023-11-21 14:11:38 +02: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 17:11:52 +02: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 14:11:38 +02:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2024-01-29 17:11:52 +02:00
k := newOidcKeySet ( cache , tt . opts ... )
2023-11-21 14:11:38 +02: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 )
} )
}
}
2024-08-23 15:43:46 +03:00
func Test_appendPublicKeysToWebKeySet ( t * testing . T ) {
keys := [ ... ] [ ] byte {
make ( [ ] byte , 32 ) ,
make ( [ ] byte , 32 ) ,
}
for _ , key := range keys {
_ , err := rand . Read ( key )
require . NoError ( t , err )
}
type args struct {
keyset * jose . JSONWebKeySet
pubkeys * query . PublicKeys
}
tests := [ ] struct {
name string
args args
want * jose . JSONWebKeySet
} {
{
name : "nil pubkeys" ,
args : args {
keyset : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
pubkeys : nil ,
} ,
want : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
} ,
{
name : "empty pubkeys" ,
args : args {
keyset : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
pubkeys : & query . PublicKeys {
Keys : [ ] query . PublicKey { } ,
} ,
} ,
want : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
} ,
{
name : "append pubkeys" ,
args : args {
keyset : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
pubkeys : & query . PublicKeys {
Keys : [ ] query . PublicKey {
& publicKey {
id : "key1" ,
key : keys [ 1 ] ,
alg : "XYZ" ,
use : crypto . KeyUsageSigning ,
} ,
} ,
} ,
} ,
want : & jose . JSONWebKeySet {
Keys : [ ] jose . JSONWebKey {
{
Key : keys [ 0 ] ,
KeyID : "key0" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
{
Key : keys [ 1 ] ,
KeyID : "key1" ,
Algorithm : "XYZ" ,
Use : crypto . KeyUsageSigning . String ( ) ,
} ,
} ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
appendPublicKeysToWebKeySet ( tt . args . keyset , tt . args . pubkeys )
assert . Equal ( t , tt . want , tt . args . keyset )
} )
}
}