// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package oidcauth
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"testing"
"time"
"github.com/coreos/go-oidc"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2/jwt"
)
func setupForJWT ( t * testing . T , authType int , f func ( c * Config ) ) ( * Authenticator , string ) {
t . Helper ( )
config := & Config {
Type : TypeJWT ,
JWTSupportedAlgs : [ ] string { oidc . ES256 } ,
ClaimMappings : map [ string ] string {
"first_name" : "name" ,
"/org/primary" : "primary_org" ,
"/nested/Size" : "size" ,
"Age" : "age" ,
"Admin" : "is_admin" ,
"/nested/division" : "division" ,
"/nested/remote" : "is_remote" ,
} ,
ListClaimMappings : map [ string ] string {
"https://go-sso/groups" : "groups" ,
} ,
}
var issuer string
switch authType {
case authOIDCDiscovery :
srv := oidcauthtest . Start ( t )
config . OIDCDiscoveryURL = srv . Addr ( )
config . OIDCDiscoveryCACert = srv . CACert ( )
issuer = config . OIDCDiscoveryURL
// TODO(sso): is this a bug in vault?
// config.BoundIssuer = issuer
case authStaticKeys :
pubKey , _ := oidcauthtest . SigningKeys ( )
config . BoundIssuer = "https://legit.issuer.internal/"
config . JWTValidationPubKeys = [ ] string { pubKey }
issuer = config . BoundIssuer
case authJWKS :
srv := oidcauthtest . Start ( t )
config . JWKSURL = srv . Addr ( ) + "/certs"
config . JWKSCACert = srv . CACert ( )
issuer = "https://legit.issuer.internal/"
// TODO(sso): is this a bug in vault?
// config.BoundIssuer = issuer
default :
require . Fail ( t , "inappropriate authType: %d" , authType )
}
if f != nil {
f ( config )
}
require . NoError ( t , config . Validate ( ) )
oa , err := New ( config , hclog . NewNullLogger ( ) )
require . NoError ( t , err )
t . Cleanup ( oa . Stop )
return oa , issuer
}
func TestJWT_OIDC_Functions_Fail ( t * testing . T ) {
t . Run ( "static" , func ( t * testing . T ) {
testJWT_OIDC_Functions_Fail ( t , authStaticKeys )
} )
t . Run ( "JWKS" , func ( t * testing . T ) {
testJWT_OIDC_Functions_Fail ( t , authJWKS )
} )
t . Run ( "oidc discovery" , func ( t * testing . T ) {
testJWT_OIDC_Functions_Fail ( t , authOIDCDiscovery )
} )
}
func testJWT_OIDC_Functions_Fail ( t * testing . T , authType int ) {
t . Helper ( )
t . Run ( "GetAuthCodeURL" , func ( t * testing . T ) {
oa , _ := setupForJWT ( t , authType , nil )
_ , err := oa . GetAuthCodeURL (
context . Background ( ) ,
"https://example.com" ,
map [ string ] string { "foo" : "bar" } ,
)
requireErrorContains ( t , err , ` GetAuthCodeURL is incompatible with type "jwt" ` )
} )
t . Run ( "ClaimsFromAuthCode" , func ( t * testing . T ) {
oa , _ := setupForJWT ( t , authType , nil )
_ , _ , err := oa . ClaimsFromAuthCode (
context . Background ( ) ,
"abc" , "def" ,
)
requireErrorContains ( t , err , ` ClaimsFromAuthCode is incompatible with type "jwt" ` )
} )
}
func TestJWT_ClaimsFromJWT ( t * testing . T ) {
t . Run ( "static" , func ( t * testing . T ) {
testJWT_ClaimsFromJWT ( t , authStaticKeys )
} )
t . Run ( "JWKS" , func ( t * testing . T ) {
testJWT_ClaimsFromJWT ( t , authJWKS )
} )
t . Run ( "oidc discovery" , func ( t * testing . T ) {
// TODO(sso): the vault versions of these tests did not run oidc-discovery
testJWT_ClaimsFromJWT ( t , authOIDCDiscovery )
} )
}
func testJWT_ClaimsFromJWT ( t * testing . T , authType int ) {
t . Helper ( )
t . Run ( "missing audience" , func ( t * testing . T ) {
if authType == authOIDCDiscovery {
// TODO(sso): why isn't this strict?
t . Skip ( "why?" )
return
}
oa , issuer := setupForJWT ( t , authType , nil )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : issuer ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Audience : jwt . Audience { "https://go-sso.test" } ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
} {
"jeff" ,
[ ] string { "foo" , "bar" } ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
_ , err = oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
requireErrorContains ( t , err , "audience claim found in JWT but no audiences are bound" )
} )
t . Run ( "valid inputs" , func ( t * testing . T ) {
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
} )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : issuer ,
Audience : jwt . Audience { "https://go-sso.test" } ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
type orgs struct {
Primary string ` json:"primary" `
}
type nested struct {
Division int64 ` json:"division" `
Remote bool ` json:"remote" `
Size string ` json:"Size" `
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
FirstName string ` json:"first_name" `
Org orgs ` json:"org" `
Color string ` json:"color" `
Age int64 ` json:"Age" `
Admin bool ` json:"Admin" `
Nested nested ` json:"nested" `
} {
User : "jeff" ,
Groups : [ ] string { "foo" , "bar" } ,
FirstName : "jeff2" ,
Org : orgs { "engineering" } ,
Color : "green" ,
Age : 85 ,
Admin : true ,
Nested : nested {
Division : 3 ,
Remote : true ,
Size : "medium" ,
} ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
claims , err := oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
require . NoError ( t , err )
expectedClaims := & Claims {
Values : map [ string ] string {
"name" : "jeff2" ,
"primary_org" : "engineering" ,
"size" : "medium" ,
"age" : "85" ,
"is_admin" : "true" ,
"division" : "3" ,
"is_remote" : "true" ,
} ,
Lists : map [ string ] [ ] string {
"groups" : { "foo" , "bar" } ,
} ,
}
require . Equal ( t , expectedClaims , claims )
} )
t . Run ( "unusable claims" , func ( t * testing . T ) {
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
} )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : issuer ,
Audience : jwt . Audience { "https://go-sso.test" } ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
type orgs struct {
Primary string ` json:"primary" `
}
type nested struct {
Division int64 ` json:"division" `
Remote bool ` json:"remote" `
Size [ ] string ` json:"Size" `
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
FirstName string ` json:"first_name" `
Org orgs ` json:"org" `
Color string ` json:"color" `
Age int64 ` json:"Age" `
Admin bool ` json:"Admin" `
Nested nested ` json:"nested" `
} {
User : "jeff" ,
Groups : [ ] string { "foo" , "bar" } ,
FirstName : "jeff2" ,
Org : orgs { "engineering" } ,
Color : "green" ,
Age : 85 ,
Admin : true ,
Nested : nested {
Division : 3 ,
Remote : true ,
Size : [ ] string { "medium" } ,
} ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
_ , err = oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
requireErrorContains ( t , err , "error converting claim '/nested/Size' to string from unknown type []interface {}" )
} )
t . Run ( "bad signature" , func ( t * testing . T ) {
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
} )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : issuer ,
Audience : jwt . Audience { "https://go-sso.test" } ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
} {
"jeff" ,
[ ] string { "foo" , "bar" } ,
}
jwtData , err := oidcauthtest . SignJWT ( badPrivKey , cl , privateCl )
require . NoError ( t , err )
_ , err = oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
switch authType {
case authOIDCDiscovery , authJWKS :
requireErrorContains ( t , err , "failed to verify id token signature" )
case authStaticKeys :
requireErrorContains ( t , err , "no known key successfully validated the token signature" )
default :
require . Fail ( t , "unexpected type: %d" , authType )
}
} )
t . Run ( "bad issuer" , func ( t * testing . T ) {
oa , _ := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
} )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : "https://not.real.issuer.internal/" ,
Audience : jwt . Audience { "https://go-sso.test" } ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
} {
"jeff" ,
[ ] string { "foo" , "bar" } ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
claims , err := oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
switch authType {
case authOIDCDiscovery :
requireErrorContains ( t , err , "error validating signature: oidc: id token issued by a different provider" )
case authStaticKeys :
requireErrorContains ( t , err , "validation failed, invalid issuer claim (iss)" )
case authJWKS :
// requireErrorContains(t, err, "validation failed, invalid issuer claim (iss)")
// TODO(sso) The original vault test doesn't care about bound issuer.
require . NoError ( t , err )
expectedClaims := & Claims {
Values : map [ string ] string { } ,
Lists : map [ string ] [ ] string {
"groups" : { "foo" , "bar" } ,
} ,
}
require . Equal ( t , expectedClaims , claims )
default :
require . Fail ( t , "unexpected type: %d" , authType )
}
} )
t . Run ( "bad audience" , func ( t * testing . T ) {
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
} )
cl := jwt . Claims {
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
Issuer : issuer ,
NotBefore : jwt . NewNumericDate ( time . Now ( ) . Add ( - 5 * time . Second ) ) ,
Audience : jwt . Audience { "https://fault.plugin.auth.jwt.test" } ,
Expiry : jwt . NewNumericDate ( time . Now ( ) . Add ( 5 * time . Second ) ) ,
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
} {
"jeff" ,
[ ] string { "foo" , "bar" } ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
_ , err = oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
requireErrorContains ( t , err , "error validating claims: aud claim does not match any bound audience" )
} )
}
func TestJWT_ClaimsFromJWT_ExpiryClaims ( t * testing . T ) {
t . Run ( "static" , func ( t * testing . T ) {
t . Parallel ( )
testJWT_ClaimsFromJWT_ExpiryClaims ( t , authStaticKeys )
} )
t . Run ( "JWKS" , func ( t * testing . T ) {
t . Parallel ( )
testJWT_ClaimsFromJWT_ExpiryClaims ( t , authJWKS )
} )
// TODO(sso): the vault versions of these tests did not run oidc-discovery
// t.Run("oidc discovery", func(t *testing.T) {
// t.Parallel()
// testJWT_ClaimsFromJWT_ExpiryClaims(t, authOIDCDiscovery)
// })
}
func testJWT_ClaimsFromJWT_ExpiryClaims ( t * testing . T , authType int ) {
t . Helper ( )
tests := map [ string ] struct {
Valid bool
IssuedAt time . Time
NotBefore time . Time
Expiration time . Time
DefaultLeeway int
ExpLeeway int
} {
// iat, auto clock_skew_leeway (60s), auto expiration leeway (150s)
"auto expire leeway using iat with auto clock_skew_leeway" : { true , time . Now ( ) . Add ( - 205 * time . Second ) , time . Time { } , time . Time { } , 0 , 0 } ,
"expired auto expire leeway using iat with auto clock_skew_leeway" : { false , time . Now ( ) . Add ( - 215 * time . Second ) , time . Time { } , time . Time { } , 0 , 0 } ,
// iat, clock_skew_leeway (10s), auto expiration leeway (150s)
"auto expire leeway using iat with custom clock_skew_leeway" : { true , time . Now ( ) . Add ( - 150 * time . Second ) , time . Time { } , time . Time { } , 10 , 0 } ,
"expired auto expire leeway using iat with custom clock_skew_leeway" : { false , time . Now ( ) . Add ( - 165 * time . Second ) , time . Time { } , time . Time { } , 10 , 0 } ,
// iat, no clock_skew_leeway (0s), auto expiration leeway (150s)
"auto expire leeway using iat with no clock_skew_leeway" : { true , time . Now ( ) . Add ( - 145 * time . Second ) , time . Time { } , time . Time { } , - 1 , 0 } ,
"expired auto expire leeway using iat with no clock_skew_leeway" : { false , time . Now ( ) . Add ( - 155 * time . Second ) , time . Time { } , time . Time { } , - 1 , 0 } ,
// nbf, auto clock_skew_leeway (60s), auto expiration leeway (150s)
"auto expire leeway using nbf with auto clock_skew_leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 205 * time . Second ) , time . Time { } , 0 , 0 } ,
"expired auto expire leeway using nbf with auto clock_skew_leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 215 * time . Second ) , time . Time { } , 0 , 0 } ,
// nbf, clock_skew_leeway (10s), auto expiration leeway (150s)
"auto expire leeway using nbf with custom clock_skew_leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 145 * time . Second ) , time . Time { } , 10 , 0 } ,
"expired auto expire leeway using nbf with custom clock_skew_leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 165 * time . Second ) , time . Time { } , 10 , 0 } ,
// nbf, no clock_skew_leeway (0s), auto expiration leeway (150s)
"auto expire leeway using nbf with no clock_skew_leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 145 * time . Second ) , time . Time { } , - 1 , 0 } ,
"expired auto expire leeway using nbf with no clock_skew_leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 155 * time . Second ) , time . Time { } , - 1 , 0 } ,
// iat, auto clock_skew_leeway (60s), custom expiration leeway (10s)
"custom expire leeway using iat with clock_skew_leeway" : { true , time . Now ( ) . Add ( - 65 * time . Second ) , time . Time { } , time . Time { } , 0 , 10 } ,
"expired custom expire leeway using iat with clock_skew_leeway" : { false , time . Now ( ) . Add ( - 75 * time . Second ) , time . Time { } , time . Time { } , 0 , 10 } ,
// iat, clock_skew_leeway (10s), custom expiration leeway (10s)
"custom expire leeway using iat with clock_skew_leeway with default leeway" : { true , time . Now ( ) . Add ( - 5 * time . Second ) , time . Time { } , time . Time { } , 10 , 10 } ,
"expired custom expire leeway using iat with clock_skew_leeway with default leeway" : { false , time . Now ( ) . Add ( - 25 * time . Second ) , time . Time { } , time . Time { } , 10 , 10 } ,
// iat, clock_skew_leeway (10s), no expiration leeway (10s)
"no expire leeway using iat with clock_skew_leeway" : { true , time . Now ( ) . Add ( - 5 * time . Second ) , time . Time { } , time . Time { } , 10 , - 1 } ,
"expired no expire leeway using iat with clock_skew_leeway" : { false , time . Now ( ) . Add ( - 15 * time . Second ) , time . Time { } , time . Time { } , 10 , - 1 } ,
// nbf, default clock_skew_leeway (60s), custom expiration leeway (10s)
"custom expire leeway using nbf with clock_skew_leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 65 * time . Second ) , time . Time { } , 0 , 10 } ,
"expired custom expire leeway using nbf with clock_skew_leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 75 * time . Second ) , time . Time { } , 0 , 10 } ,
// nbf, clock_skew_leeway (10s), custom expiration leeway (0s)
"custom expire leeway using nbf with clock_skew_leeway with default leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 5 * time . Second ) , time . Time { } , 10 , 10 } ,
"expired custom expire leeway using nbf with clock_skew_leeway with default leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 25 * time . Second ) , time . Time { } , 10 , 10 } ,
// nbf, clock_skew_leeway (10s), no expiration leeway (0s)
"no expire leeway using nbf with clock_skew_leeway with default leeway" : { true , time . Time { } , time . Now ( ) . Add ( - 5 * time . Second ) , time . Time { } , 10 , - 1 } ,
"no expire leeway using nbf with clock_skew_leeway with default leeway and nbf" : { true , time . Time { } , time . Now ( ) . Add ( - 5 * time . Second ) , time . Time { } , 10 , - 100 } ,
"expired no expire leeway using nbf with clock_skew_leeway" : { false , time . Time { } , time . Now ( ) . Add ( - 15 * time . Second ) , time . Time { } , 10 , - 1 } ,
"expired no expire leeway using nbf with clock_skew_leeway with default leeway and nbf" : { false , time . Time { } , time . Now ( ) . Add ( - 15 * time . Second ) , time . Time { } , 10 , - 100 } ,
}
for name , tt := range tests {
tt := tt
t . Run ( name , func ( t * testing . T ) {
t . Parallel ( )
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
c . ClockSkewLeeway = time . Duration ( tt . DefaultLeeway ) * time . Second
c . ExpirationLeeway = time . Duration ( tt . ExpLeeway ) * time . Second
c . NotBeforeLeeway = 0
} )
jwtData := setupLogin ( t , tt . IssuedAt , tt . Expiration , tt . NotBefore , issuer )
_ , err := oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
if tt . Valid {
require . NoError ( t , err )
} else {
require . Error ( t , err )
}
} )
}
}
func TestJWT_ClaimsFromJWT_NotBeforeClaims ( t * testing . T ) {
t . Run ( "static" , func ( t * testing . T ) {
t . Parallel ( )
testJWT_ClaimsFromJWT_NotBeforeClaims ( t , authStaticKeys )
} )
t . Run ( "JWKS" , func ( t * testing . T ) {
t . Parallel ( )
testJWT_ClaimsFromJWT_NotBeforeClaims ( t , authJWKS )
} )
// TODO(sso): the vault versions of these tests did not run oidc-discovery
// t.Run("oidc discovery", func(t *testing.T) {
// t.Parallel()
// testJWT_ClaimsFromJWT_NotBeforeClaims(t, authOIDCDiscovery)
// })
}
func testJWT_ClaimsFromJWT_NotBeforeClaims ( t * testing . T , authType int ) {
t . Helper ( )
tests := map [ string ] struct {
Valid bool
IssuedAt time . Time
NotBefore time . Time
Expiration time . Time
DefaultLeeway int
NBFLeeway int
} {
// iat, auto clock_skew_leeway (60s), no nbf leeway (0)
"no nbf leeway using iat with auto clock_skew_leeway" : { true , time . Now ( ) . Add ( 55 * time . Second ) , time . Time { } , time . Now ( ) , 0 , - 1 } ,
"not yet valid no nbf leeway using iat with auto clock_skew_leeway" : { false , time . Now ( ) . Add ( 65 * time . Second ) , time . Time { } , time . Now ( ) , 0 , - 1 } ,
// iat, clock_skew_leeway (10s), no nbf leeway (0s)
"no nbf leeway using iat with custom clock_skew_leeway" : { true , time . Now ( ) . Add ( 5 * time . Second ) , time . Time { } , time . Time { } , 10 , - 1 } ,
"not yet valid no nbf leeway using iat with custom clock_skew_leeway" : { false , time . Now ( ) . Add ( 15 * time . Second ) , time . Time { } , time . Time { } , 10 , - 1 } ,
// iat, no clock_skew_leeway (0s), nbf leeway (5s)
"nbf leeway using iat with no clock_skew_leeway" : { true , time . Now ( ) , time . Time { } , time . Time { } , - 1 , 5 } ,
"not yet valid nbf leeway using iat with no clock_skew_leeway" : { false , time . Now ( ) . Add ( 6 * time . Second ) , time . Time { } , time . Time { } , - 1 , 5 } ,
// exp, auto clock_skew_leeway (60s), auto nbf leeway (150s)
"auto nbf leeway using exp with auto clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 205 * time . Second ) , 0 , 0 } ,
"not yet valid auto nbf leeway using exp with auto clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 215 * time . Second ) , 0 , 0 } ,
// exp, clock_skew_leeway (10s), auto nbf leeway (150s)
"auto nbf leeway using exp with custom clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 150 * time . Second ) , 10 , 0 } ,
"not yet valid auto nbf leeway using exp with custom clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 165 * time . Second ) , 10 , 0 } ,
// exp, no clock_skew_leeway (0s), auto nbf leeway (150s)
"auto nbf leeway using exp with no clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 145 * time . Second ) , - 1 , 0 } ,
"not yet valid auto nbf leeway using exp with no clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 152 * time . Second ) , - 1 , 0 } ,
// exp, auto clock_skew_leeway (60s), custom nbf leeway (10s)
"custom nbf leeway using exp with auto clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 65 * time . Second ) , 0 , 10 } ,
"not yet valid custom nbf leeway using exp with auto clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 75 * time . Second ) , 0 , 10 } ,
// exp, clock_skew_leeway (10s), custom nbf leeway (10s)
"custom nbf leeway using exp with custom clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 15 * time . Second ) , 10 , 10 } ,
"not yet valid custom nbf leeway using exp with custom clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 25 * time . Second ) , 10 , 10 } ,
// exp, no clock_skew_leeway (0s), custom nbf leeway (5s)
"custom nbf leeway using exp with no clock_skew_leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 3 * time . Second ) , - 1 , 5 } ,
"custom nbf leeway using exp with no clock_skew_leeway with default leeway" : { true , time . Time { } , time . Time { } , time . Now ( ) . Add ( 3 * time . Second ) , - 100 , 5 } ,
"not yet valid custom nbf leeway using exp with no clock_skew_leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 7 * time . Second ) , - 1 , 5 } ,
"not yet valid custom nbf leeway using exp with no clock_skew_leeway with default leeway" : { false , time . Time { } , time . Time { } , time . Now ( ) . Add ( 7 * time . Second ) , - 100 , 5 } ,
}
for name , tt := range tests {
tt := tt
t . Run ( name , func ( t * testing . T ) {
t . Parallel ( )
oa , issuer := setupForJWT ( t , authType , func ( c * Config ) {
c . BoundAudiences = [ ] string {
"https://go-sso.test" ,
"another_audience" ,
}
c . ClockSkewLeeway = time . Duration ( tt . DefaultLeeway ) * time . Second
c . ExpirationLeeway = 0
c . NotBeforeLeeway = time . Duration ( tt . NBFLeeway ) * time . Second
} )
jwtData := setupLogin ( t , tt . IssuedAt , tt . Expiration , tt . NotBefore , issuer )
_ , err := oa . ClaimsFromJWT ( context . Background ( ) , jwtData )
if tt . Valid {
require . NoError ( t , err )
} else {
require . Error ( t , err )
}
} )
}
}
func setupLogin ( t * testing . T , iat , exp , nbf time . Time , issuer string ) string {
cl := jwt . Claims {
Audience : jwt . Audience { "https://go-sso.test" } ,
Issuer : issuer ,
Subject : "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients" ,
IssuedAt : jwt . NewNumericDate ( iat ) ,
Expiry : jwt . NewNumericDate ( exp ) ,
NotBefore : jwt . NewNumericDate ( nbf ) ,
}
privateCl := struct {
User string ` json:"https://go-sso/user" `
Groups [ ] string ` json:"https://go-sso/groups" `
Color string ` json:"color" `
} {
"foobar" ,
[ ] string { "foo" , "bar" } ,
"green" ,
}
jwtData , err := oidcauthtest . SignJWT ( "" , cl , privateCl )
require . NoError ( t , err )
return jwtData
}
func TestParsePublicKeyPEM ( t * testing . T ) {
if testing . Short ( ) {
t . Skip ( "too slow for testing.Short" )
}
getPublicPEM := func ( t * testing . T , pub interface { } ) string {
derBytes , err := x509 . MarshalPKIXPublicKey ( pub )
require . NoError ( t , err )
pemBlock := & pem . Block {
Type : "PUBLIC KEY" ,
Bytes : derBytes ,
}
return string ( pem . EncodeToMemory ( pemBlock ) )
}
t . Run ( "rsa" , func ( t * testing . T ) {
privateKey , err := rsa . GenerateKey ( rand . Reader , 2048 )
require . NoError ( t , err )
pub := privateKey . Public ( )
pubPEM := getPublicPEM ( t , pub )
got , err := parsePublicKeyPEM ( [ ] byte ( pubPEM ) )
require . NoError ( t , err )
require . Equal ( t , pub , got )
} )
t . Run ( "ecdsa" , func ( t * testing . T ) {
privateKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
require . NoError ( t , err )
pub := privateKey . Public ( )
pubPEM := getPublicPEM ( t , pub )
got , err := parsePublicKeyPEM ( [ ] byte ( pubPEM ) )
require . NoError ( t , err )
require . Equal ( t , pub , got )
} )
t . Run ( "ed25519" , func ( t * testing . T ) {
pub , _ , err := ed25519 . GenerateKey ( rand . Reader )
require . NoError ( t , err )
pubPEM := getPublicPEM ( t , pub )
got , err := parsePublicKeyPEM ( [ ] byte ( pubPEM ) )
require . NoError ( t , err )
require . Equal ( t , pub , got )
} )
}
const (
badPrivKey string = ` -- -- - BEGIN EC PRIVATE KEY -- -- -
MHcCAQEEILTAHJm + clBKYCrRDc74Pt7uF7kH + 2 x2TdL5cH23FEcsoAoGCCqGSM49
AwEHoUQDQgAE + C3CyjVWdeYtIqgluFJlwZmoonphsQbj9Nfo5wrEutv + 3 RTFnDQh
vttUajcFAcl4beR + jHFYC00vSO4i5jZ64g ==
-- -- - END EC PRIVATE KEY -- -- - `
)