2023-03-30 10:54:09 +02:00
package jwtclaims
2021-08-12 12:49:10 +02:00
import (
2024-07-03 11:33:02 +02:00
"context"
2024-08-27 16:37:55 +02:00
"crypto/ecdsa"
"crypto/elliptic"
2022-12-07 22:06:43 +01:00
"crypto/rsa"
"encoding/base64"
2021-08-12 12:49:10 +02:00
"encoding/json"
"errors"
2022-12-07 22:06:43 +01:00
"fmt"
"math/big"
2021-08-12 12:49:10 +02:00
"net/http"
2023-04-14 11:20:34 +02:00
"strconv"
"strings"
2023-04-19 16:11:38 +02:00
"sync"
2023-04-14 11:20:34 +02:00
"time"
2023-02-03 21:47:20 +01:00
"github.com/golang-jwt/jwt"
log "github.com/sirupsen/logrus"
2021-08-12 12:49:10 +02:00
)
2023-03-30 10:54:09 +02:00
// Options is a struct for specifying configuration options for the middleware.
type Options struct {
// The function that will return the Key to validate the JWT.
// It can be either a shared secret or a public key.
// Default value: nil
ValidationKeyGetter jwt . Keyfunc
// The name of the property in the request where the user information
// from the JWT will be stored.
// Default value: "user"
UserProperty string
// The function that will be called when there's an error validating the token
// Default value:
CredentialsOptional bool
// A function that extracts the token from the request
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
Debug bool
// When set, all requests with the OPTIONS method will use authentication
// Default: false
EnableAuthOnOptions bool
}
2022-12-07 22:06:43 +01:00
// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
2021-08-12 12:49:10 +02:00
type Jwks struct {
2023-04-14 11:20:34 +02:00
Keys [ ] JSONWebKey ` json:"keys" `
expiresInTime time . Time
2021-08-12 12:49:10 +02:00
}
2024-08-27 16:37:55 +02:00
// The supported elliptic curves types
const (
// p256 represents a cryptographic elliptical curve type.
p256 = "P-256"
// p384 represents a cryptographic elliptical curve type.
p384 = "P-384"
// p521 represents a cryptographic elliptical curve type.
p521 = "P-521"
)
2022-12-07 22:06:43 +01:00
// JSONWebKey is a representation of a Jason Web Key
type JSONWebKey struct {
2021-08-12 12:49:10 +02:00
Kty string ` json:"kty" `
Kid string ` json:"kid" `
Use string ` json:"use" `
N string ` json:"n" `
E string ` json:"e" `
2024-08-27 16:37:55 +02:00
Crv string ` json:"crv" `
X string ` json:"x" `
Y string ` json:"y" `
2021-08-12 12:49:10 +02:00
X5c [ ] string ` json:"x5c" `
}
2023-03-30 17:32:44 +02:00
// JWTValidator struct to handle token validation and parsing
2023-03-30 10:54:09 +02:00
type JWTValidator struct {
options Options
}
2024-11-29 10:06:40 +01:00
var keyNotFound = errors . New ( "unable to find appropriate key" )
2023-03-30 17:32:44 +02:00
// NewJWTValidator constructor
2024-07-03 11:33:02 +02:00
func NewJWTValidator ( ctx context . Context , issuer string , audienceList [ ] string , keysLocation string , idpSignkeyRefreshEnabled bool ) ( * JWTValidator , error ) {
keys , err := getPemKeys ( ctx , keysLocation )
2021-08-12 12:49:10 +02:00
if err != nil {
return nil , err
}
2023-04-19 16:11:38 +02:00
var lock sync . Mutex
2023-03-30 10:54:09 +02:00
options := Options {
2021-08-12 12:49:10 +02:00
ValidationKeyGetter : func ( token * jwt . Token ) ( interface { } , error ) {
// Verify 'aud' claim
2023-04-04 16:40:56 +02:00
var checkAud bool
for _ , audience := range audienceList {
checkAud = token . Claims . ( jwt . MapClaims ) . VerifyAudience ( audience , false )
if checkAud {
break
}
}
2021-08-12 12:49:10 +02:00
if ! checkAud {
return token , errors . New ( "invalid audience" )
}
// Verify 'issuer' claim
checkIss := token . Claims . ( jwt . MapClaims ) . VerifyIssuer ( issuer , false )
if ! checkIss {
return token , errors . New ( "invalid issuer" )
}
2023-04-14 11:20:34 +02:00
// If keys are rotated, verify the keys prior to token validation
2023-04-15 02:44:42 +02:00
if idpSignkeyRefreshEnabled {
2023-04-14 11:20:34 +02:00
// If the keys are invalid, retrieve new ones
if ! keys . stillValid ( ) {
2023-04-19 16:11:38 +02:00
lock . Lock ( )
defer lock . Unlock ( )
2023-04-21 12:34:52 +02:00
2024-07-03 11:33:02 +02:00
refreshedKeys , err := getPemKeys ( ctx , keysLocation )
2023-04-14 11:20:34 +02:00
if err != nil {
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Debugf ( "cannot get JSONWebKey: %v, falling back to old keys" , err )
2023-04-21 12:34:52 +02:00
refreshedKeys = keys
2023-04-14 11:20:34 +02:00
}
2023-04-21 12:34:52 +02:00
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Debugf ( "keys refreshed, new UTC expiration time: %s" , refreshedKeys . expiresInTime . UTC ( ) )
2023-12-12 14:56:27 +01:00
2023-04-21 12:34:52 +02:00
keys = refreshedKeys
2023-04-14 11:20:34 +02:00
}
}
2024-08-27 16:37:55 +02:00
publicKey , err := getPublicKey ( ctx , token , keys )
2024-11-29 10:06:40 +01:00
if err == nil {
return publicKey , nil
}
msg := fmt . Sprintf ( "getPublicKey error: %s" , err )
if errors . Is ( err , keyNotFound ) && ! idpSignkeyRefreshEnabled {
msg = fmt . Sprintf ( "getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service" , err )
2021-08-12 12:49:10 +02:00
}
2024-11-29 10:06:40 +01:00
log . WithContext ( ctx ) . Error ( msg )
return nil , err
2021-08-12 12:49:10 +02:00
} ,
2021-08-16 11:29:57 +02:00
EnableAuthOnOptions : false ,
2023-03-30 10:54:09 +02:00
}
if options . UserProperty == "" {
options . UserProperty = "user"
}
return & JWTValidator {
options : options ,
} , nil
}
2023-03-30 17:32:44 +02:00
// ValidateAndParse validates the token and returns the parsed token
2024-07-03 11:33:02 +02:00
func ( m * JWTValidator ) ValidateAndParse ( ctx context . Context , token string ) ( * jwt . Token , error ) {
2023-03-30 10:54:09 +02:00
// If the token is empty...
if token == "" {
// Check if it was required
if m . options . CredentialsOptional {
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Debugf ( "no credentials found (CredentialsOptional=true)" )
2023-03-30 10:54:09 +02:00
// No error, just no token (and that is ok given that CredentialsOptional is true)
2023-09-04 17:03:44 +02:00
return nil , nil //nolint:nilnil
2023-03-30 10:54:09 +02:00
}
// If we get here, the required token is missing
errorMsg := "required authorization token not found"
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Debugf ( " Error: No credentials found (CredentialsOptional=false)" )
2024-08-14 12:30:10 +02:00
return nil , errors . New ( errorMsg )
2023-03-30 10:54:09 +02:00
}
// Now parse the token
parsedToken , err := jwt . Parse ( token , m . options . ValidationKeyGetter )
// Check if there was an error in parsing...
if err != nil {
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Errorf ( "error parsing token: %v" , err )
2024-08-27 16:37:55 +02:00
return nil , fmt . Errorf ( "error parsing token: %w" , err )
2023-03-30 10:54:09 +02:00
}
// Check if the parsed token is valid...
if ! parsedToken . Valid {
errorMsg := "token is invalid"
2024-08-14 12:30:10 +02:00
log . WithContext ( ctx ) . Debug ( errorMsg )
2023-03-30 10:54:09 +02:00
return nil , errors . New ( errorMsg )
}
return parsedToken , nil
2021-08-12 12:49:10 +02:00
}
2023-04-14 11:20:34 +02:00
// stillValid returns true if the JSONWebKey still valid and have enough time to be used
func ( jwks * Jwks ) stillValid ( ) bool {
2023-12-12 14:56:27 +01:00
return ! jwks . expiresInTime . IsZero ( ) && time . Now ( ) . Add ( 5 * time . Second ) . Before ( jwks . expiresInTime )
2023-04-14 11:20:34 +02:00
}
2024-07-03 11:33:02 +02:00
func getPemKeys ( ctx context . Context , keysLocation string ) ( * Jwks , error ) {
2021-08-12 12:49:10 +02:00
resp , err := http . Get ( keysLocation )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
2023-02-03 21:47:20 +01:00
jwks := & Jwks { }
2021-08-12 12:49:10 +02:00
err = json . NewDecoder ( resp . Body ) . Decode ( jwks )
if err != nil {
return jwks , err
}
2023-04-14 11:20:34 +02:00
cacheControlHeader := resp . Header . Get ( "Cache-Control" )
2024-07-03 11:33:02 +02:00
expiresIn := getMaxAgeFromCacheHeader ( ctx , cacheControlHeader )
2023-04-14 11:20:34 +02:00
jwks . expiresInTime = time . Now ( ) . Add ( time . Duration ( expiresIn ) * time . Second )
2021-08-12 12:49:10 +02:00
return jwks , err
}
2024-08-27 16:37:55 +02:00
func getPublicKey ( ctx context . Context , token * jwt . Token , jwks * Jwks ) ( interface { } , error ) {
2022-12-07 22:06:43 +01:00
// todo as we load the jkws when the server is starting, we should build a JKS map with the pem cert at the boot time
2021-08-12 12:49:10 +02:00
for k := range jwks . Keys {
2022-12-07 22:06:43 +01:00
if token . Header [ "kid" ] != jwks . Keys [ k ] . Kid {
continue
}
if len ( jwks . Keys [ k ] . X5c ) != 0 {
2024-08-27 16:37:55 +02:00
cert := "-----BEGIN CERTIFICATE-----\n" + jwks . Keys [ k ] . X5c [ 0 ] + "\n-----END CERTIFICATE-----"
return jwt . ParseRSAPublicKeyFromPEM ( [ ] byte ( cert ) )
2021-08-12 12:49:10 +02:00
}
2022-12-07 22:06:43 +01:00
2024-08-27 16:37:55 +02:00
if jwks . Keys [ k ] . Kty == "RSA" {
log . WithContext ( ctx ) . Debugf ( "generating PublicKey from RSA JWK" )
return getPublicKeyFromRSA ( jwks . Keys [ k ] )
}
if jwks . Keys [ k ] . Kty == "EC" {
log . WithContext ( ctx ) . Debugf ( "generating PublicKey from ECDSA JWK" )
return getPublicKeyFromECDSA ( jwks . Keys [ k ] )
}
2022-12-07 22:06:43 +01:00
2024-08-27 16:37:55 +02:00
log . WithContext ( ctx ) . Debugf ( "Key Type: %s not yet supported, please raise ticket!" , jwks . Keys [ k ] . Kty )
2022-12-07 22:06:43 +01:00
}
2024-11-29 10:06:40 +01:00
return nil , keyNotFound
2024-08-27 16:37:55 +02:00
}
2022-12-07 22:06:43 +01:00
2024-08-27 16:37:55 +02:00
func getPublicKeyFromECDSA ( jwk JSONWebKey ) ( publicKey * ecdsa . PublicKey , err error ) {
2022-12-07 22:06:43 +01:00
2024-08-27 16:37:55 +02:00
if jwk . X == "" || jwk . Y == "" || jwk . Crv == "" {
return nil , fmt . Errorf ( "ecdsa key incomplete" )
2022-12-07 22:06:43 +01:00
}
2024-08-27 16:37:55 +02:00
var xCoordinate [ ] byte
if xCoordinate , err = base64 . RawURLEncoding . DecodeString ( jwk . X ) ; err != nil {
return nil , err
2022-12-07 22:06:43 +01:00
}
2024-08-27 16:37:55 +02:00
var yCoordinate [ ] byte
if yCoordinate , err = base64 . RawURLEncoding . DecodeString ( jwk . Y ) ; err != nil {
return nil , err
2022-12-07 22:06:43 +01:00
}
2024-08-27 16:37:55 +02:00
publicKey = & ecdsa . PublicKey { }
var curve elliptic . Curve
switch jwk . Crv {
case p256 :
curve = elliptic . P256 ( )
case p384 :
curve = elliptic . P384 ( )
case p521 :
curve = elliptic . P521 ( )
2021-08-12 12:49:10 +02:00
}
2024-08-27 16:37:55 +02:00
publicKey . Curve = curve
publicKey . X = big . NewInt ( 0 ) . SetBytes ( xCoordinate )
publicKey . Y = big . NewInt ( 0 ) . SetBytes ( yCoordinate )
return publicKey , nil
2022-12-07 22:06:43 +01:00
}
2024-08-27 16:37:55 +02:00
func getPublicKeyFromRSA ( jwk JSONWebKey ) ( * rsa . PublicKey , error ) {
decodedE , err := base64 . RawURLEncoding . DecodeString ( jwk . E )
2022-12-07 22:06:43 +01:00
if err != nil {
2024-08-27 16:37:55 +02:00
return nil , err
2022-12-07 22:06:43 +01:00
}
2024-08-27 16:37:55 +02:00
decodedN , err := base64 . RawURLEncoding . DecodeString ( jwk . N )
2022-12-07 22:06:43 +01:00
if err != nil {
2024-08-27 16:37:55 +02:00
return nil , err
2021-08-12 12:49:10 +02:00
}
2024-08-27 16:37:55 +02:00
var n , e big . Int
e . SetBytes ( decodedE )
n . SetBytes ( decodedN )
return & rsa . PublicKey {
E : int ( e . Int64 ( ) ) ,
N : & n ,
} , nil
2021-08-12 12:49:10 +02:00
}
2023-04-14 11:20:34 +02:00
// getMaxAgeFromCacheHeader extracts max-age directive from the Cache-Control header
2024-07-03 11:33:02 +02:00
func getMaxAgeFromCacheHeader ( ctx context . Context , cacheControl string ) int {
2023-04-14 11:20:34 +02:00
// Split into individual directives
directives := strings . Split ( cacheControl , "," )
for _ , directive := range directives {
directive = strings . TrimSpace ( directive )
if strings . HasPrefix ( directive , "max-age=" ) {
// Extract the max-age value
maxAgeStr := strings . TrimPrefix ( directive , "max-age=" )
maxAge , err := strconv . Atoi ( maxAgeStr )
if err != nil {
2024-07-03 11:33:02 +02:00
log . WithContext ( ctx ) . Debugf ( "error parsing max-age: %v" , err )
2023-04-14 11:20:34 +02:00
return 0
}
return maxAge
}
}
return 0
}